OLD | NEW |
(Empty) | |
| 1 # gitpoller.py taken from buildbot/build/third_party/buildbot_8_2/buildbot/chang
es/gitpoller.py |
| 2 # |
| 3 # URL: http://buildbot.net |
| 4 # Sources: http://github.com/buildbot/buildbot |
| 5 # Version: 0.8.2 |
| 6 # License: GNU General Public License (GPL) Version 2 |
| 7 # |
| 8 # Hack together to provide the feature in buildbot7 |
| 9 # Not placed in third_party since it's a mish-mash. |
| 10 # |
| 11 # All Chromium changes are labeled with 'Chromium change'. Current changes: |
| 12 # 1) On git error, print the error to the log so you can debug. |
| 13 # 2) Do NOT clear the environment when doing git commands; it throws |
| 14 # away permissions (perhaps by deleting HOME so we can't find our SSH |
| 15 # keys). 2 spots. |
| 16 # 3) Add get_file_contents() function. |
| 17 |
| 18 |
| 19 import time |
| 20 import tempfile |
| 21 import os |
| 22 import subprocess |
| 23 |
| 24 import select |
| 25 import errno |
| 26 |
| 27 from twisted.python import log, failure |
| 28 from twisted.internet import reactor, utils |
| 29 from twisted.internet.task import LoopingCall |
| 30 |
| 31 from buildbot.changes import base, changes |
| 32 |
| 33 class GitPoller(base.ChangeSource): |
| 34 """This source will poll a remote git repo for changes and submit |
| 35 them to the change master.""" |
| 36 |
| 37 compare_attrs = ["repourl", "branch", "workdir", |
| 38 "pollinterval", "gitbin", "usetimestamps", |
| 39 "category", "project"] |
| 40 |
| 41 parent = None # filled in when we're added |
| 42 loop = None |
| 43 volatile = ['loop'] |
| 44 working = False |
| 45 running = False |
| 46 |
| 47 def __init__(self, repourl, branch='master', |
| 48 workdir=None, pollinterval=10*60, |
| 49 gitbin='git', usetimestamps=True, |
| 50 category=None, project=''): |
| 51 """ |
| 52 @type repourl: string |
| 53 @param repourl: the url that describes the remote repository, |
| 54 e.g. git:foobaz/myrepo.git |
| 55 |
| 56 @type branch: string |
| 57 @param branch: the desired branch to fetch, will default to 'master' |
| 58 |
| 59 @type workdir: string |
| 60 @param workdir: the directory where the poller should keep its local rep
ository. |
| 61 will default to <tempdir>/gitpoller_work |
| 62 |
| 63 @type pollinterval: int |
| 64 @param pollinterval: interval in seconds between polls, default is 10 mi
nutes |
| 65 |
| 66 @type gitbin: string |
| 67 @param gitbin: path to the git binary, defaults to just 'git' |
| 68 |
| 69 @type usetimestamps: boolean |
| 70 @param usetimestamps: parse each revision's commit timestamp (default Tr
ue), or |
| 71 ignore it in favor of the current time (to appear
together |
| 72 in the waterfall page) |
| 73 |
| 74 @type category: string |
| 75 @param category: catergory associated with the change. Attached to |
| 76 the Change object produced by this changesource suc
h that |
| 77 it can be targeted by change filters. |
| 78 |
| 79 @type project string |
| 80 @param project project that the changes are associated to. Attache
d to |
| 81 the Change object produced by this changesource suc
h that |
| 82 it can be targeted by change filters. |
| 83 """ |
| 84 |
| 85 self.repourl = repourl |
| 86 self.branch = branch |
| 87 self.pollinterval = pollinterval |
| 88 self.lastChange = time.time() |
| 89 self.lastPoll = time.time() |
| 90 self.gitbin = gitbin |
| 91 self.workdir = workdir |
| 92 self.usetimestamps = usetimestamps |
| 93 self.category = category |
| 94 self.project = project |
| 95 |
| 96 if self.workdir == None: |
| 97 self.workdir = tempfile.gettempdir() + '/gitpoller_work' |
| 98 |
| 99 def startService(self): |
| 100 self.loop = LoopingCall(self.poll) |
| 101 base.ChangeSource.startService(self) |
| 102 |
| 103 if not os.path.exists(self.workdir): |
| 104 log.msg('gitpoller: creating working dir %s' % self.workdir) |
| 105 os.makedirs(self.workdir) |
| 106 |
| 107 if not os.path.exists(self.workdir + r'/.git'): |
| 108 log.msg('gitpoller: initializing working dir') |
| 109 os.system(self.gitbin + ' clone ' + self.repourl + ' ' + self.workdi
r) |
| 110 |
| 111 reactor.callLater(0, self.loop.start, self.pollinterval) |
| 112 |
| 113 self.running = True |
| 114 |
| 115 def stopService(self): |
| 116 if self.running: |
| 117 self.loop.stop() |
| 118 self.running = False |
| 119 return base.ChangeSource.stopService(self) |
| 120 |
| 121 def describe(self): |
| 122 status = "" |
| 123 if not self.running: |
| 124 status = "[STOPPED - check log]" |
| 125 str = 'GitPoller watching the remote git repository %s, branch: %s %s' \ |
| 126 % (self.repourl, self.branch, status) |
| 127 return str |
| 128 |
| 129 def poll(self): |
| 130 if self.working: |
| 131 log.msg('gitpoller: not polling git repo because last poll is still
working') |
| 132 else: |
| 133 self.working = True |
| 134 d = self._get_changes() |
| 135 |
| 136 d.addCallback(self._process_changes) |
| 137 d.addCallbacks(self._changes_finished_ok, self._changes_finished_fai
lure) |
| 138 d.addCallback(self._catch_up) |
| 139 d.addCallbacks(self._catch_up_finished_ok, self._catch_up__finished_
failure) |
| 140 return |
| 141 |
| 142 def get_file_contents(self, file_path): |
| 143 log.msg('getting contents of file %s' % file_path) |
| 144 # Need to catch up HEAD, since this can get called during the |
| 145 # process_changes callback path. |
| 146 p = subprocess.Popen(['git', 'reset', '--hard', 'FETCH_HEAD'], |
| 147 cwd=self.workdir) |
| 148 p.communicate() |
| 149 |
| 150 p = subprocess.Popen(['cat', file_path], cwd=self.workdir, |
| 151 stdout=subprocess.PIPE) |
| 152 output, _ = p.communicate() |
| 153 |
| 154 return output |
| 155 |
| 156 def _get_git_output(self, args): |
| 157 git_args = [self.gitbin] + args |
| 158 |
| 159 p = subprocess.Popen(git_args, |
| 160 cwd=self.workdir, |
| 161 stdout=subprocess.PIPE) |
| 162 |
| 163 # dirty hack - work around EINTR oddness on Mac builder |
| 164 while True: |
| 165 try: |
| 166 output = p.communicate()[0] |
| 167 break |
| 168 except (OSError, select.error), e: |
| 169 if e[0] == errno.EINTR: |
| 170 continue |
| 171 else: |
| 172 raise |
| 173 |
| 174 if p.returncode != 0: |
| 175 raise EnvironmentError('call \'%s\' exited with error \'%s\', output
: \'%s\'' % |
| 176 (args, p.returncode, output)) |
| 177 return output |
| 178 |
| 179 def _get_commit_comments(self, rev): |
| 180 args = ['log', rev, '--no-walk', r'--format=%s%n%b'] |
| 181 output = self._get_git_output(args) |
| 182 |
| 183 if len(output.strip()) == 0: |
| 184 raise EnvironmentError('could not get commit comment for rev %s' % r
ev) |
| 185 |
| 186 return output |
| 187 |
| 188 def _get_commit_timestamp(self, rev): |
| 189 # unix timestamp |
| 190 args = ['log', rev, '--no-walk', r'--format=%ct'] |
| 191 output = self._get_git_output(args) |
| 192 |
| 193 try: |
| 194 stamp = float(output) |
| 195 except Exception, e: |
| 196 log.msg('gitpoller: caught exception converting output \'%s\' to tim
estamp' % output) |
| 197 raise e |
| 198 |
| 199 return stamp |
| 200 |
| 201 def _get_commit_files(self, rev): |
| 202 args = ['log', rev, '--name-only', '--no-walk', r'--format=%n'] |
| 203 fileList = self._get_git_output(args).split() |
| 204 return fileList |
| 205 |
| 206 def _get_commit_name(self, rev): |
| 207 args = ['log', rev, '--no-walk', r'--format=%cn'] |
| 208 output = self._get_git_output(args) |
| 209 |
| 210 if len(output.strip()) == 0: |
| 211 raise EnvironmentError('could not get commit name for rev %s' % rev) |
| 212 |
| 213 return output |
| 214 |
| 215 def _get_changes(self): |
| 216 log.msg('gitpoller: polling git repo at %s' % self.repourl) |
| 217 |
| 218 self.lastPoll = time.time() |
| 219 |
| 220 # get a deferred object that performs the fetch |
| 221 args = ['fetch', self.repourl, self.branch] |
| 222 # Chromium change: if we null out the environment we get git permission
errors. E.g. |
| 223 # 'git fetch' will 'Permission denied (publickey,gssapi-keyex,gssapi-wit
h-mic,password).' |
| 224 # For Clank we just use the environment. |
| 225 env = os.environ |
| 226 d = utils.getProcessOutput(self.gitbin, args, path=self.workdir, env=env
, errortoo=1 ) |
| 227 |
| 228 return d |
| 229 |
| 230 def _process_changes(self, res): |
| 231 # get the change list |
| 232 |
| 233 # Chromium change: make sure errors are super-clear |
| 234 if 'Permission denied' in res: |
| 235 print res |
| 236 |
| 237 revListArgs = ['log', 'HEAD..FETCH_HEAD', r'--format=%H'] |
| 238 revs = self._get_git_output(revListArgs); |
| 239 revCount = 0 |
| 240 |
| 241 # process oldest change first |
| 242 revList = revs.split() |
| 243 if revList: |
| 244 revList.reverse() |
| 245 revCount = len(revList) |
| 246 |
| 247 log.msg('gitpoller: processing %d changes' % revCount ) |
| 248 |
| 249 for rev in revList: |
| 250 if self.usetimestamps: |
| 251 commit_timestamp = self._get_commit_timestamp(rev) |
| 252 else: |
| 253 commit_timestamp = None # use current time |
| 254 |
| 255 c = changes.Change(who = self._get_commit_name(rev), |
| 256 revision = rev, |
| 257 files = self._get_commit_files(rev), |
| 258 comments = self._get_commit_comments(rev), |
| 259 when = commit_timestamp, |
| 260 branch = self.branch, |
| 261 category = self.category, |
| 262 # CHANGED(bradnelson): project doesn't exist on |
| 263 # our bb8 version. |
| 264 #project = self.project, |
| 265 repository = self.repourl) |
| 266 self.parent.addChange(c) |
| 267 self.lastChange = self.lastPoll |
| 268 |
| 269 def _catch_up(self, res): |
| 270 log.msg('gitpoller: catching up to FETCH_HEAD') |
| 271 |
| 272 args = ['reset', '--hard', 'FETCH_HEAD'] |
| 273 # Chromium change: use the env |
| 274 env = os.environ |
| 275 d = utils.getProcessOutputAndValue(self.gitbin, args, path=self.workdir,
env=env) |
| 276 return d; |
| 277 |
| 278 def _changes_finished_ok(self, res): |
| 279 assert self.working |
| 280 # check for failure -- this is probably never hit but the twisted docs |
| 281 # are not clear enough to be sure. it is being kept "just in case" |
| 282 if isinstance(res, failure.Failure): |
| 283 return self._changes_finished_failure(res) |
| 284 |
| 285 return res |
| 286 |
| 287 def _changes_finished_failure(self, res): |
| 288 log.msg('gitpoller: repo poll failed: %s' % res) |
| 289 assert self.working |
| 290 # eat the failure to continue along the defered chain |
| 291 # - we still want to catch up |
| 292 return None |
| 293 |
| 294 def _catch_up_finished_ok(self, res): |
| 295 assert self.working |
| 296 |
| 297 # check for failure -- this is probably never hit but the twisted docs |
| 298 # are not clear enough to be sure. it is being kept "just in case" |
| 299 if isinstance(res, failure.Failure): |
| 300 return self._catch_up__finished_failure(res) |
| 301 |
| 302 elif isinstance(res, tuple): |
| 303 (stdout, stderr, code) = res |
| 304 if code != 0: |
| 305 e = EnvironmentError('catch up failed with exit code: %d' % code
) |
| 306 return self._catch_up__finished_failure(failure.Failure(e)) |
| 307 |
| 308 self.working = False |
| 309 return res |
| 310 |
| 311 def _catch_up__finished_failure(self, res): |
| 312 assert self.working |
| 313 assert isinstance(res, failure.Failure) |
| 314 self.working = False |
| 315 |
| 316 log.msg('gitpoller: catch up failed: %s' % res) |
| 317 log.msg('gitpoller: stopping service - please resolve issues in local re
po: %s' % |
| 318 self.workdir) |
| 319 self.stopService() |
| 320 return res |
OLD | NEW |