blob: 2340d8bea20982655e5e2c17f1d35f4a72720757 [file] [log] [blame]
import datetime
import logging
import os
import subprocess
import tempfile
import dulwich.repo
from django.conf import settings
from patchwork.parser import parse_patch
log = logging.getLogger('gitrepo')
def croncmd(args, cwd='./', timeout=None, get_fail_logger=None):
if timeout:
args = ['timeout', str(timeout)] + args
with tempfile.SpooledTemporaryFile(max_size=4096) as f:
try:
subprocess.check_call(args, cwd=cwd, stdout=f, stderr=f)
if log.level == logging.DEBUG:
log.debug('Results of cmd(%s): %r', cwd, args)
f.seek()
log.debug('COMBINED_OUTPUT\n%s', f.read())
except subprocess.CalledProcessError as e:
logger = log.error
if get_fail_logger:
logger = get_fail_logger()
logger('Unable to run command(%s): %r', cwd, args)
if timeout and e.returncode == 124:
logger('Command timed out')
f.seek(0)
if e.output:
logger('STDOUT:\n%s', e.output)
logger('STDERR:\n%s', f.read())
else:
logger('COMBINED OUTPUT:\n%s', f.read())
raise
class Repo(object):
'''Our patchwork deployments try and automatically update patches by
looking at the change history on a repository. This class provides a
simple interface to analyze new commits
'''
def __init__(self, repo_dir, name, scm_url):
self.path = os.path.join(repo_dir, name)
self.scm_url = scm_url
self._repo = None
@property
def repo(self):
if not self._repo:
self._repo = dulwich.repo.Repo(self.path)
return self._repo
def __getitem__(self, key):
return self.repo[key]
def _clone(self):
croncmd(['git', 'clone', '--mirror', self.scm_url, self.path])
def _pull(self):
fail_file = os.path.join(self.path, 'failures')
def get_fail_logger():
with open(fail_file, 'a+') as f:
f.write('failed at: %s\n' % str(datetime.datetime.now()))
f.seek(0)
for count, line in enumerate(f, 1):
if count > 3:
return log.error
return log.info
timeout = str(getattr(settings, 'REPO_TIMEOUT', 120))
croncmd(
['git', 'remote', '-v', 'update', '--prune'], self.path, timeout,
get_fail_logger)
if os.path.exists(fail_file):
# clean out old failures, now that we've succeeded
os.unlink(fail_file)
def update(self):
if not os.path.exists(self.path):
self._clone()
else:
try:
self._pull()
except subprocess.CalledProcessError:
# We've already logged the exception. Code can continue to run
# because its just going to call process_unchecked_commits and
# essentially be a no-op
pass
def process_unchecked_commits(self, save_state=True):
last_commit = os.path.join(self.path, 'patchwork-last-commit')
if os.path.exists(last_commit):
with open(last_commit) as f:
start_at = f.read().strip()
else:
start_at = 'HEAD~100'
log.debug('looking for commits since: %s', start_at)
args = ['git', 'rev-list', '--reverse', start_at + '..HEAD']
with open('/dev/null', 'w') as f:
rc = subprocess.call(
['git', 'show', start_at], cwd=self.path, stdout=f, stderr=f)
if rc != 0:
# we may have had a branch who's history was re-written
# just try and get changes for past day
args = ['git', 'rev-list', '--reverse',
'--since', '1 day ago', 'HEAD']
try:
for x in subprocess.check_output(args, cwd=self.path).split('\n'):
if x:
yield self.repo[x]
start_at = x
finally:
if save_state:
with open(last_commit, 'w') as f:
f.write(start_at)
def get_patch(self, commit):
args = ['git', 'show', '--format=format:%e', '-M', str(commit.id)]
patch = subprocess.check_output(args, cwd=self.path)
# the patchwork parser code operates character by character so we must
# convert to unicode so it can be handled properly
patch = patch.decode('utf-8', errors='replace')
# Don't try and process >5Mb patches, they flood the server
if len(patch) > 5000000:
raise MemoryError('patch too large to process: %d' % len(patch))
patch = parse_patch(patch)[0]
if patch is None:
# happens for binary only patches like:
# https://git.linaro.org/uefi/OpenPlatformPkg.git/commit/ \
# ?id=7ab4bb34b2464a2491868264bdf2931f2acd6452
patch = ''
return patch