| Index: scripts/master/tryjob_git_poller.py
|
| ===================================================================
|
| --- scripts/master/tryjob_git_poller.py (revision 0)
|
| +++ scripts/master/tryjob_git_poller.py (revision 0)
|
| @@ -0,0 +1,320 @@
|
| +# gitpoller.py taken from buildbot/build/third_party/buildbot_8_2/buildbot/changes/gitpoller.py
|
| +#
|
| +# URL: http://buildbot.net
|
| +# Sources: http://github.com/buildbot/buildbot
|
| +# Version: 0.8.2
|
| +# License: GNU General Public License (GPL) Version 2
|
| +#
|
| +# Hack together to provide the feature in buildbot7
|
| +# Not placed in third_party since it's a mish-mash.
|
| +#
|
| +# All Chromium changes are labeled with 'Chromium change'. Current changes:
|
| +# 1) On git error, print the error to the log so you can debug.
|
| +# 2) Do NOT clear the environment when doing git commands; it throws
|
| +# away permissions (perhaps by deleting HOME so we can't find our SSH
|
| +# keys). 2 spots.
|
| +# 3) Add get_file_contents() function.
|
| +
|
| +
|
| +import time
|
| +import tempfile
|
| +import os
|
| +import subprocess
|
| +
|
| +import select
|
| +import errno
|
| +
|
| +from twisted.python import log, failure
|
| +from twisted.internet import reactor, utils
|
| +from twisted.internet.task import LoopingCall
|
| +
|
| +from buildbot.changes import base, changes
|
| +
|
| +class GitPoller(base.ChangeSource):
|
| + """This source will poll a remote git repo for changes and submit
|
| + them to the change master."""
|
| +
|
| + compare_attrs = ["repourl", "branch", "workdir",
|
| + "pollinterval", "gitbin", "usetimestamps",
|
| + "category", "project"]
|
| +
|
| + parent = None # filled in when we're added
|
| + loop = None
|
| + volatile = ['loop']
|
| + working = False
|
| + running = False
|
| +
|
| + def __init__(self, repourl, branch='master',
|
| + workdir=None, pollinterval=10*60,
|
| + gitbin='git', usetimestamps=True,
|
| + category=None, project=''):
|
| + """
|
| + @type repourl: string
|
| + @param repourl: the url that describes the remote repository,
|
| + e.g. git:foobaz/myrepo.git
|
| +
|
| + @type branch: string
|
| + @param branch: the desired branch to fetch, will default to 'master'
|
| +
|
| + @type workdir: string
|
| + @param workdir: the directory where the poller should keep its local repository.
|
| + will default to <tempdir>/gitpoller_work
|
| +
|
| + @type pollinterval: int
|
| + @param pollinterval: interval in seconds between polls, default is 10 minutes
|
| +
|
| + @type gitbin: string
|
| + @param gitbin: path to the git binary, defaults to just 'git'
|
| +
|
| + @type usetimestamps: boolean
|
| + @param usetimestamps: parse each revision's commit timestamp (default True), or
|
| + ignore it in favor of the current time (to appear together
|
| + in the waterfall page)
|
| +
|
| + @type category: string
|
| + @param category: catergory associated with the change. Attached to
|
| + the Change object produced by this changesource such that
|
| + it can be targeted by change filters.
|
| +
|
| + @type project string
|
| + @param project project that the changes are associated to. Attached to
|
| + the Change object produced by this changesource such that
|
| + it can be targeted by change filters.
|
| + """
|
| +
|
| + self.repourl = repourl
|
| + self.branch = branch
|
| + self.pollinterval = pollinterval
|
| + self.lastChange = time.time()
|
| + self.lastPoll = time.time()
|
| + self.gitbin = gitbin
|
| + self.workdir = workdir
|
| + self.usetimestamps = usetimestamps
|
| + self.category = category
|
| + self.project = project
|
| +
|
| + if self.workdir == None:
|
| + self.workdir = tempfile.gettempdir() + '/gitpoller_work'
|
| +
|
| + def startService(self):
|
| + self.loop = LoopingCall(self.poll)
|
| + base.ChangeSource.startService(self)
|
| +
|
| + if not os.path.exists(self.workdir):
|
| + log.msg('gitpoller: creating working dir %s' % self.workdir)
|
| + os.makedirs(self.workdir)
|
| +
|
| + if not os.path.exists(self.workdir + r'/.git'):
|
| + log.msg('gitpoller: initializing working dir')
|
| + os.system(self.gitbin + ' clone ' + self.repourl + ' ' + self.workdir)
|
| +
|
| + reactor.callLater(0, self.loop.start, self.pollinterval)
|
| +
|
| + self.running = True
|
| +
|
| + def stopService(self):
|
| + if self.running:
|
| + self.loop.stop()
|
| + self.running = False
|
| + return base.ChangeSource.stopService(self)
|
| +
|
| + def describe(self):
|
| + status = ""
|
| + if not self.running:
|
| + status = "[STOPPED - check log]"
|
| + str = 'GitPoller watching the remote git repository %s, branch: %s %s' \
|
| + % (self.repourl, self.branch, status)
|
| + return str
|
| +
|
| + def poll(self):
|
| + if self.working:
|
| + log.msg('gitpoller: not polling git repo because last poll is still working')
|
| + else:
|
| + self.working = True
|
| + d = self._get_changes()
|
| +
|
| + d.addCallback(self._process_changes)
|
| + d.addCallbacks(self._changes_finished_ok, self._changes_finished_failure)
|
| + d.addCallback(self._catch_up)
|
| + d.addCallbacks(self._catch_up_finished_ok, self._catch_up__finished_failure)
|
| + return
|
| +
|
| + def get_file_contents(self, file_path):
|
| + log.msg('getting contents of file %s' % file_path)
|
| + # Need to catch up HEAD, since this can get called during the
|
| + # process_changes callback path.
|
| + p = subprocess.Popen(['git', 'reset', '--hard', 'FETCH_HEAD'],
|
| + cwd=self.workdir)
|
| + p.communicate()
|
| +
|
| + p = subprocess.Popen(['cat', file_path], cwd=self.workdir,
|
| + stdout=subprocess.PIPE)
|
| + output, _ = p.communicate()
|
| +
|
| + return output
|
| +
|
| + def _get_git_output(self, args):
|
| + git_args = [self.gitbin] + args
|
| +
|
| + p = subprocess.Popen(git_args,
|
| + cwd=self.workdir,
|
| + stdout=subprocess.PIPE)
|
| +
|
| + # dirty hack - work around EINTR oddness on Mac builder
|
| + while True:
|
| + try:
|
| + output = p.communicate()[0]
|
| + break
|
| + except (OSError, select.error), e:
|
| + if e[0] == errno.EINTR:
|
| + continue
|
| + else:
|
| + raise
|
| +
|
| + if p.returncode != 0:
|
| + raise EnvironmentError('call \'%s\' exited with error \'%s\', output: \'%s\'' %
|
| + (args, p.returncode, output))
|
| + return output
|
| +
|
| + def _get_commit_comments(self, rev):
|
| + args = ['log', rev, '--no-walk', r'--format=%s%n%b']
|
| + output = self._get_git_output(args)
|
| +
|
| + if len(output.strip()) == 0:
|
| + raise EnvironmentError('could not get commit comment for rev %s' % rev)
|
| +
|
| + return output
|
| +
|
| + def _get_commit_timestamp(self, rev):
|
| + # unix timestamp
|
| + args = ['log', rev, '--no-walk', r'--format=%ct']
|
| + output = self._get_git_output(args)
|
| +
|
| + try:
|
| + stamp = float(output)
|
| + except Exception, e:
|
| + log.msg('gitpoller: caught exception converting output \'%s\' to timestamp' % output)
|
| + raise e
|
| +
|
| + return stamp
|
| +
|
| + def _get_commit_files(self, rev):
|
| + args = ['log', rev, '--name-only', '--no-walk', r'--format=%n']
|
| + fileList = self._get_git_output(args).split()
|
| + return fileList
|
| +
|
| + def _get_commit_name(self, rev):
|
| + args = ['log', rev, '--no-walk', r'--format=%cn']
|
| + output = self._get_git_output(args)
|
| +
|
| + if len(output.strip()) == 0:
|
| + raise EnvironmentError('could not get commit name for rev %s' % rev)
|
| +
|
| + return output
|
| +
|
| + def _get_changes(self):
|
| + log.msg('gitpoller: polling git repo at %s' % self.repourl)
|
| +
|
| + self.lastPoll = time.time()
|
| +
|
| + # get a deferred object that performs the fetch
|
| + args = ['fetch', self.repourl, self.branch]
|
| + # Chromium change: if we null out the environment we get git permission errors. E.g.
|
| + # 'git fetch' will 'Permission denied (publickey,gssapi-keyex,gssapi-with-mic,password).'
|
| + # For Clank we just use the environment.
|
| + env = os.environ
|
| + d = utils.getProcessOutput(self.gitbin, args, path=self.workdir, env=env, errortoo=1 )
|
| +
|
| + return d
|
| +
|
| + def _process_changes(self, res):
|
| + # get the change list
|
| +
|
| + # Chromium change: make sure errors are super-clear
|
| + if 'Permission denied' in res:
|
| + print res
|
| +
|
| + revListArgs = ['log', 'HEAD..FETCH_HEAD', r'--format=%H']
|
| + revs = self._get_git_output(revListArgs);
|
| + revCount = 0
|
| +
|
| + # process oldest change first
|
| + revList = revs.split()
|
| + if revList:
|
| + revList.reverse()
|
| + revCount = len(revList)
|
| +
|
| + log.msg('gitpoller: processing %d changes' % revCount )
|
| +
|
| + for rev in revList:
|
| + if self.usetimestamps:
|
| + commit_timestamp = self._get_commit_timestamp(rev)
|
| + else:
|
| + commit_timestamp = None # use current time
|
| +
|
| + c = changes.Change(who = self._get_commit_name(rev),
|
| + revision = rev,
|
| + files = self._get_commit_files(rev),
|
| + comments = self._get_commit_comments(rev),
|
| + when = commit_timestamp,
|
| + branch = self.branch,
|
| + category = self.category,
|
| + # CHANGED(bradnelson): project doesn't exist on
|
| + # our bb8 version.
|
| + #project = self.project,
|
| + repository = self.repourl)
|
| + self.parent.addChange(c)
|
| + self.lastChange = self.lastPoll
|
| +
|
| + def _catch_up(self, res):
|
| + log.msg('gitpoller: catching up to FETCH_HEAD')
|
| +
|
| + args = ['reset', '--hard', 'FETCH_HEAD']
|
| + # Chromium change: use the env
|
| + env = os.environ
|
| + d = utils.getProcessOutputAndValue(self.gitbin, args, path=self.workdir, env=env)
|
| + return d;
|
| +
|
| + def _changes_finished_ok(self, res):
|
| + assert self.working
|
| + # check for failure -- this is probably never hit but the twisted docs
|
| + # are not clear enough to be sure. it is being kept "just in case"
|
| + if isinstance(res, failure.Failure):
|
| + return self._changes_finished_failure(res)
|
| +
|
| + return res
|
| +
|
| + def _changes_finished_failure(self, res):
|
| + log.msg('gitpoller: repo poll failed: %s' % res)
|
| + assert self.working
|
| + # eat the failure to continue along the defered chain
|
| + # - we still want to catch up
|
| + return None
|
| +
|
| + def _catch_up_finished_ok(self, res):
|
| + assert self.working
|
| +
|
| + # check for failure -- this is probably never hit but the twisted docs
|
| + # are not clear enough to be sure. it is being kept "just in case"
|
| + if isinstance(res, failure.Failure):
|
| + return self._catch_up__finished_failure(res)
|
| +
|
| + elif isinstance(res, tuple):
|
| + (stdout, stderr, code) = res
|
| + if code != 0:
|
| + e = EnvironmentError('catch up failed with exit code: %d' % code)
|
| + return self._catch_up__finished_failure(failure.Failure(e))
|
| +
|
| + self.working = False
|
| + return res
|
| +
|
| + def _catch_up__finished_failure(self, res):
|
| + assert self.working
|
| + assert isinstance(res, failure.Failure)
|
| + self.working = False
|
| +
|
| + log.msg('gitpoller: catch up failed: %s' % res)
|
| + log.msg('gitpoller: stopping service - please resolve issues in local repo: %s' %
|
| + self.workdir)
|
| + self.stopService()
|
| + return res
|
|
|
| Property changes on: scripts/master/tryjob_git_poller.py
|
| ___________________________________________________________________
|
| Added: svn:eol-style
|
| + LF
|
|
|
|
|