Support HTTP-only Gerrit

This adds support for performing queries and git clones over HTTP
(reporting over HTTP was already supported).  This will happen
automatically for any connection with a password configured.
Otherwise, the SSH connection will be used as before.

Change-Id: I11920a13615de103eb3d8fb305eacbbcb30e5e40
This commit is contained in:
James E. Blair 2019-09-12 14:33:53 -07:00
parent 4d8325a3cc
commit 947b7b1dcb
5 changed files with 473 additions and 152 deletions

View File

@ -42,6 +42,7 @@ import time
import uuid
import socketserver
import http.server
import urllib.parse
import git
import gear
@ -188,6 +189,7 @@ class FakeGerritChange(object):
self.subject = subject
self.latest_patchset = 0
self.depends_on_change = None
self.depends_on_patchset = None
self.needed_by_changes = []
self.fail_merge = False
self.messages = []
@ -490,13 +492,14 @@ class FakeGerritChange(object):
def setDependsOn(self, other, patchset):
self.depends_on_change = other
self.depends_on_patchset = patchset
d = {'id': other.data['id'],
'number': other.data['number'],
'ref': other.patchsets[patchset - 1]['ref']
}
self.data['dependsOn'] = [d]
other.needed_by_changes.append(self)
other.needed_by_changes.append((self, len(self.patchsets)))
needed = other.data.get('neededBy', [])
d = {'id': self.data['id'],
'number': self.data['number'],
@ -517,6 +520,100 @@ class FakeGerritChange(object):
d['isCurrentPatchSet'] = False
return json.loads(json.dumps(self.data))
def queryHTTP(self):
self.queried += 1
labels = {}
for cat in self.categories:
labels[cat] = {}
for app in self.patchsets[-1]['approvals']:
label = labels[app['type']]
_, label_min, label_max = self.categories[app['type']]
val = int(app['value'])
label_all = label.setdefault('all', [])
label_all.append({
"value": val,
"username": app['by']['username'],
"email": app['by']['email'],
"date": str(datetime.datetime.fromtimestamp(app['grantedOn'])),
})
if val == label_min:
label['blocking'] = True
if 'rejected' not in label:
label['rejected'] = app['by']
if val == label_max:
if 'approved' not in label:
label['approved'] = app['by']
revisions = {}
rev = self.patchsets[-1]
num = len(self.patchsets)
files = {}
for f in rev['files']:
files[f['file']] = {"status": f['type'][0]} # ADDED -> A
parent = '0000000000000000000000000000000000000000'
if self.depends_on_change:
parent = self.depends_on_change.patchsets[
self.depends_on_patchset - 1]['revision']
revisions[rev['revision']] = {
"kind": "REWORK",
"_number": num,
"created": rev['createdOn'],
"uploader": rev['uploader'],
"ref": rev['ref'],
"commit": {
"subject": self.subject,
"message": self.data['commitMessage'],
"parents": [{
"commit": parent,
}]
},
"files": files
}
data = {
"id": self.project + '~' + self.branch + '~' + self.data['id'],
"project": self.project,
"branch": self.branch,
"hashtags": [],
"change_id": self.data['id'],
"subject": self.subject,
"status": self.data['status'],
"created": self.data['createdOn'],
"updated": self.data['lastUpdated'],
"_number": self.number,
"owner": self.data['owner'],
"labels": labels,
"current_revision": self.patchsets[-1]['revision'],
"revisions": revisions,
"requirements": []
}
return json.loads(json.dumps(data))
def queryRevisionHTTP(self, revision):
for ps in self.patchsets:
if ps['revision'] == revision:
break
else:
return None
changes = []
if self.depends_on_change:
changes.append({
"commit": {
"commit": self.depends_on_change.patchsets[
self.depends_on_patchset - 1]['revision'],
},
"_change_number": self.depends_on_change.number,
"_revision_number": self.depends_on_patchset
})
for (needed_by_change, needed_by_patchset) in self.needed_by_changes:
changes.append({
"commit": {
"commit": needed_by_change.patchsets[
needed_by_patchset - 1]['revision'],
},
"_change_number": needed_by_change.number,
"_revision_number": needed_by_patchset,
})
return {"changes": changes}
def setMerged(self):
if (self.depends_on_change and
self.depends_on_change.data['status'] != 'MERGED'):
@ -554,6 +651,9 @@ class GerritWebServer(object):
update_checks_re = re.compile(
r'/a/changes/(.*)/revisions/(.*?)/checks/(.*)')
list_checkers_re = re.compile('/a/plugins/checks/checkers/')
change_re = re.compile(r'/a/changes/(.*)\?o=.*')
related_re = re.compile(r'/a/changes/(.*)/revisions/(.*)/related')
change_search_re = re.compile(r'/a/changes/\?n=500.*&q=(.*)')
def do_POST(self):
path = self.path
@ -580,6 +680,15 @@ class GerritWebServer(object):
path = self.path
self.log.debug("Got GET %s", path)
m = self.change_re.match(path)
if m:
return self.get_change(m.group(1))
m = self.related_re.match(path)
if m:
return self.get_related(m.group(1), m.group(2))
m = self.change_search_re.match(path)
if m:
return self.get_changes(m.group(1))
m = self.pending_checks_re.match(path)
if m:
return self.get_pending_checks(m.group(1), m.group(2))
@ -663,6 +772,40 @@ class GerritWebServer(object):
self.log.debug("Get checkers")
self.send_data(fake_gerrit.fake_checkers)
def get_change(self, number):
change = fake_gerrit.changes.get(int(number))
if not change:
return self._404()
self.send_data(change.queryHTTP())
self.end_headers()
def get_related(self, number, revision):
change = fake_gerrit.changes.get(int(number))
if not change:
return self._404()
data = change.queryRevisionHTTP(revision)
if data is None:
return self._404()
self.send_data(data)
self.end_headers()
def get_changes(self, query):
self.log.debug("simpleQueryHTTP: %s", query)
query = urllib.parse.unquote(query)
fake_gerrit.queries.append(query)
results = []
if query.startswith('(') and 'OR' in query:
query = query[1:-1]
for q in query.split(' OR '):
for r in fake_gerrit._simpleQuery(q, http=True):
if r not in results:
results.append(r)
else:
results = fake_gerrit._simpleQuery(query, http=True)
self.send_data(results)
self.end_headers()
def send_data(self, data):
data = json.dumps(data).encode('utf-8')
data = b")]}'\n" + data
@ -852,25 +995,27 @@ class FakeGerritConnection(gerritconnection.GerritConnection):
if message:
change.setReported()
def query(self, number, event=None):
if type(number) == int:
return self.queryChange(number, event=event)
raise Exception("Could not query %s %s" % (type(number, number)))
def queryChange(self, number, event=None):
def queryChangeSSH(self, number, event=None):
self.log.debug("Query change SSH: %s", number)
change = self.changes.get(int(number))
if change:
return change.query()
return {}
def _simpleQuery(self, query):
def _simpleQuery(self, query, http=False):
if http:
def queryMethod(change):
return change.queryHTTP()
else:
def queryMethod(change):
return change.query()
# the query can be in parenthesis so strip them if needed
if query.startswith('('):
query = query[1:-1]
if query.startswith('change:'):
# Query a specific changeid
changeid = query[len('change:'):]
l = [change.query() for change in self.changes.values()
l = [queryMethod(change) for change in self.changes.values()
if (change.data['id'] == changeid or
change.data['number'] == changeid)]
elif query.startswith('message:'):
@ -879,16 +1024,16 @@ class FakeGerritConnection(gerritconnection.GerritConnection):
# Remove quoting if it is there
if msg.startswith('{') and msg.endswith('}'):
msg = msg[1:-1]
l = [change.query() for change in self.changes.values()
l = [queryMethod(change) for change in self.changes.values()
if msg in change.data['commitMessage']]
else:
# Query all open changes
l = [change.query() for change in self.changes.values()]
l = [queryMethod(change) for change in self.changes.values()]
return l
def simpleQuery(self, query, event=None):
def simpleQuerySSH(self, query, event=None):
log = get_annotated_logger(self.log, event)
log.debug("simpleQuery: %s", query)
log.debug("simpleQuerySSH: %s", query)
self.queries.append(query)
results = []
if query.startswith('(') and 'OR' in query:

View File

@ -22,6 +22,7 @@ URL_FORMATS = [
'{baseurl}/{change_no}',
'{baseurl}/#/c/{change_no}',
'{baseurl}/c/{project}/+/{change_no}/',
'{change_id}',
]
@ -60,7 +61,8 @@ class TestGerritCRD(ZuulTestCase):
url = url_fmt.format(baseurl=B.gerrit.baseurl.rstrip('/'),
project=B.project,
change_no=B.number)
change_no=B.number,
change_id=B.data['id'])
A.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
A.subject, url)
@ -101,6 +103,7 @@ class TestGerritCRD(ZuulTestCase):
# changes - repeat the simple test on each of the 3 to ensure they can be
# parsed, the other tests just use the default URL schema provided in
# FakeGerritChange.data['url'] .
# This list also includes the legacy change id.
def test_crd_gate_url_schema0(self):
self._test_crd_gate(URL_FORMATS[0])
@ -110,6 +113,9 @@ class TestGerritCRD(ZuulTestCase):
def test_crd_gate_url_schema2(self):
self._test_crd_gate(URL_FORMATS[2])
def test_crd_gate_legacy_id(self):
self._test_crd_gate(URL_FORMATS[3])
def test_crd_gate_triangle(self):
A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A')
B = self.fake_gerrit.addFakeChange('org/project2', 'master', 'B')

View File

@ -35,7 +35,6 @@ from uuid import uuid4
from zuul.connection import BaseConnection
from zuul.lib.logutil import get_annotated_logger
from zuul.model import Ref, Tag, Branch, Project
from zuul import exceptions
from zuul.driver.gerrit.gerritmodel import GerritChange, GerritTriggerEvent
from zuul.driver.gerrit.auth import FormAuth
from zuul import version as zuul_version
@ -44,6 +43,68 @@ from zuul import version as zuul_version
TIMEOUT = 30
class GerritChangeData(object):
"""Compatability layer for SSH/HTTP
This class holds the raw data returned from a change query over
SSH or HTTP. Most of the work of parsing the data and storing it
on the change is in the gerritmodel.GerritChange class, however
this does perform a small amount of parsing of dependencies since
they are handled outside of that class. This provides an API to
that data independent of the source.
"""
SSH = 1
HTTP = 2
def __init__(self, fmt, data, related=None):
self.format = fmt
self.data = data
if fmt == self.SSH:
self.parseSSH(data)
else:
self.parseHTTP(data)
if related:
self.parseRelatedHTTP(data, related)
def parseSSH(self, data):
self.needed_by = []
self.depends_on = None
self.message = data['commitMessage']
self.current_patchset = str(data['currentPatchSet']['number'])
self.number = str(data['number'])
if 'dependsOn' in data:
parts = data['dependsOn'][0]['ref'].split('/')
self.depends_on = (parts[3], parts[4])
for needed in data.get('neededBy', []):
parts = needed['ref'].split('/')
self.needed_by.append((parts[3], parts[4]))
def parseHTTP(self, data):
rev = data['revisions'][data['current_revision']]
self.message = rev['commit']['message']
self.current_patchset = str(rev['_number'])
self.number = str(data['_number'])
def parseRelatedHTTP(self, data, related):
self.needed_by = []
self.depends_on = None
current_rev = data['revisions'][data['current_revision']]
for change in related['changes']:
for parent in current_rev['commit']['parents']:
if change['commit']['commit'] == parent['commit']:
self.depends_on = (change['_change_number'],
change['_revision_number'])
break
else:
self.needed_by.append((change['_change_number'],
change['_revision_number']))
class GerritEventConnector(threading.Thread):
"""Move events from Gerrit to the scheduler."""
@ -380,6 +441,13 @@ class GerritConnection(BaseConnection):
self.port = int(self.connection_config.get('port', 29418))
self.keyfile = self.connection_config.get('sshkey', None)
self.keepalive = int(self.connection_config.get('keepalive', 60))
# TODO(corvus): Document this when the checks api is stable;
# it's not useful without it.
self.enable_stream_events = self.connection_config.get(
'stream_events', True)
if self.enable_stream_events not in [
'true', 'True', '1', 1, 'TRUE', True]:
self.enable_stream_events = False
self.watcher_thread = None
self.poller_thread = None
self.event_queue = queue.Queue()
@ -627,7 +695,7 @@ class GerritConnection(BaseConnection):
log.debug("Updating %s: Running query %s to find needed changes",
change, query)
records.extend(self.simpleQuery(query, event=event))
return records
return [(x.number, x.current_patchset) for x in records]
def _getNeededByFromCommit(self, change_id, change, event):
log = get_annotated_logger(self.log, event)
@ -639,11 +707,10 @@ class GerritConnection(BaseConnection):
results = self.simpleQuery(query, event=event)
for result in results:
for match in self.depends_on_re.findall(
result['commitMessage']):
result.message):
if match != change_id:
continue
key = (str(result['number']),
str(result['currentPatchSet']['number']))
key = (result.number, result.current_patchset)
if key in seen:
continue
log.debug("Updating %s: Found change %s,%s "
@ -651,7 +718,7 @@ class GerritConnection(BaseConnection):
change, key[0], key[1], change_id)
seen.add(key)
records.append(result)
return records
return [(x.number, x.current_patchset) for x in records]
def _updateChange(self, change, event, history):
log = get_annotated_logger(self.log, event)
@ -679,54 +746,16 @@ class GerritConnection(BaseConnection):
log.info("Updating %s", change)
data = self.queryChange(change.number, event=event)
change._data = data
change.update(data, self)
if change.patchset is None:
change.patchset = str(data['currentPatchSet']['number'])
if 'project' not in data:
raise exceptions.ChangeNotFound(change.number, change.patchset)
change.project = self.source.getProject(data['project'])
change.id = data['id']
change.branch = data['branch']
change.url = data['url']
urlparse = urllib.parse.urlparse(self.baseurl)
baseurl = "%s%s" % (urlparse.netloc, urlparse.path)
baseurl = baseurl.rstrip('/')
change.uris = [
'%s/%s' % (baseurl, change.number),
'%s/#/c/%s' % (baseurl, change.number),
'%s/c/%s/+/%s' % (baseurl, change.project.name, change.number),
]
if not change.is_merged:
self._updateChangeDependencies(log, change, data, event, history)
max_ps = 0
files = []
for ps in data['patchSets']:
if str(ps['number']) == change.patchset:
change.ref = ps['ref']
change.commit = ps['revision']
for f in ps.get('files', []):
files.append(f['file'])
if int(ps['number']) > int(max_ps):
max_ps = str(ps['number'])
if max_ps == change.patchset:
change.is_current_patchset = True
else:
change.is_current_patchset = False
change.files = files
self.sched.onChangeUpdated(change, event)
change.is_merged = self._isMerged(change)
change.approvals = data['currentPatchSet'].get('approvals', [])
change.open = data['open']
change.status = data['status']
change.owner = data['owner']
change.message = data['commitMessage']
if change.is_merged:
# This change is merged, so we don't need to look any further
# for dependencies.
log.debug("Updating %s: change is merged", change)
return change
return change
def _updateChangeDependencies(self, log, change, data, event, history):
if history is None:
history = []
else:
@ -735,9 +764,8 @@ class GerritConnection(BaseConnection):
needs_changes = set()
git_needs_changes = []
if 'dependsOn' in data:
parts = data['dependsOn'][0]['ref'].split('/')
dep_num, dep_ps = parts[3], parts[4]
if data.depends_on is not None:
dep_num, dep_ps = data.depends_on
log.debug("Updating %s: Getting git-dependent change %s,%s",
change, dep_num, dep_ps)
dep = self._getChange(dep_num, dep_ps, history=history,
@ -751,10 +779,8 @@ class GerritConnection(BaseConnection):
change.git_needs_changes = git_needs_changes
compat_needs_changes = []
for record in self._getDependsOnFromCommit(data['commitMessage'],
change, event):
dep_num = str(record['number'])
dep_ps = str(record['currentPatchSet']['number'])
for (dep_num, dep_ps) in self._getDependsOnFromCommit(
change.message, change, event):
log.debug("Updating %s: Getting commit-dependent "
"change %s,%s", change, dep_num, dep_ps)
dep = self._getChange(dep_num, dep_ps, history=history,
@ -766,24 +792,20 @@ class GerritConnection(BaseConnection):
needed_by_changes = set()
git_needed_by_changes = []
if 'neededBy' in data:
for needed in data['neededBy']:
parts = needed['ref'].split('/')
dep_num, dep_ps = parts[3], parts[4]
log.debug("Updating %s: Getting git-needed change %s,%s",
change, dep_num, dep_ps)
dep = self._getChange(dep_num, dep_ps, history=history,
event=event)
if (dep.open and dep.is_current_patchset and
dep not in needed_by_changes):
git_needed_by_changes.append(dep)
needed_by_changes.add(dep)
for (dep_num, dep_ps) in data.needed_by:
log.debug("Updating %s: Getting git-needed change %s,%s",
change, dep_num, dep_ps)
dep = self._getChange(dep_num, dep_ps, history=history,
event=event)
if (dep.open and dep.is_current_patchset and
dep not in needed_by_changes):
git_needed_by_changes.append(dep)
needed_by_changes.add(dep)
change.git_needed_by_changes = git_needed_by_changes
compat_needed_by_changes = []
for record in self._getNeededByFromCommit(data['id'], change, event):
dep_num = str(record['number'])
dep_ps = str(record['currentPatchSet']['number'])
for (dep_num, dep_ps) in self._getNeededByFromCommit(
change.id, change, event):
log.debug("Updating %s: Getting commit-needed change %s,%s",
change, dep_num, dep_ps)
# Because a commit needed-by may be a cross-repo
@ -800,10 +822,6 @@ class GerritConnection(BaseConnection):
needed_by_changes.add(dep)
change.compat_needed_by_changes = compat_needed_by_changes
self.sched.onChangeUpdated(change, event)
return change
def isMerged(self, change, head=None):
self.log.debug("Checking if change %s is merged" % change)
if not change.number:
@ -813,8 +831,7 @@ class GerritConnection(BaseConnection):
return True
data = self.queryChange(change.number)
change._data = data
change.is_merged = self._isMerged(change)
change.update(data, self)
if change.is_merged:
self.log.debug("Change %s is merged" % (change,))
else:
@ -834,17 +851,6 @@ class GerritConnection(BaseConnection):
(change))
return False
def _isMerged(self, change):
data = change._data
if not data:
return False
status = data.get('status')
if not status:
return False
if status == 'MERGED':
return True
return False
def _waitForRefSha(self, project: Project,
ref: str, old_sha: str='') -> bool:
# Wait for the ref to show up in the repo
@ -873,35 +879,9 @@ class GerritConnection(BaseConnection):
# Good question. It's probably ref-updated, which, ah,
# means it's merged.
return True
data = change._data
if not data:
return False
if 'submitRecords' not in data:
return False
try:
for sr in data['submitRecords']:
if sr['status'] == 'OK':
return True
elif sr['status'] == 'NOT_READY':
for label in sr['labels']:
if label['status'] in ['OK', 'MAY']:
continue
elif label['status'] in ['NEED', 'REJECT']:
# It may be our own rejection, so we ignore
if label['label'] not in allow_needs:
return False
continue
else:
# IMPOSSIBLE
return False
else:
# CLOSED, RULE_ERROR
return False
except Exception:
log.exception("Exception determining whether change"
"%s can merge:", change)
return False
return True
if change.missing_labels < set(allow_needs):
return True
return False
def getProjectOpenChanges(self, project: Project) -> List[GerritChange]:
# This is a best-effort function in case Gerrit is unable to return
@ -914,11 +894,10 @@ class GerritConnection(BaseConnection):
for record in data:
try:
changes.append(
self._getChange(record['number'],
record['currentPatchSet']['number']))
self._getChange(record.number, record.current_patchset))
except Exception:
self.log.exception("Unable to query change %s" %
(record.get('number'),))
self.log.exception("Unable to query change %s",
record.number)
return changes
@staticmethod
@ -1065,12 +1044,11 @@ class GerritConnection(BaseConnection):
"Error submitting data to gerrit, attempt %s", x)
time.sleep(x * 10)
def query(self, query, event=None):
def queryChangeSSH(self, number, event=None):
args = '--all-approvals --comments --commit-message'
args += ' --current-patch-set --dependencies --files'
args += ' --patch-sets --submit-records'
cmd = 'gerrit query --format json %s %s' % (
args, query)
cmd = 'gerrit query --format json %s %s' % (args, number)
out, err = self._ssh(cmd)
if not out:
return False
@ -1085,10 +1063,23 @@ class GerritConnection(BaseConnection):
pprint.pformat(data))
return data
def queryChange(self, number, event=None):
return self.query('change:%s' % number, event=event)
def queryChangeHTTP(self, number, event=None):
data = self.get('changes/%s?o=DETAILED_ACCOUNTS&o=CURRENT_REVISION&'
'o=CURRENT_COMMIT&o=CURRENT_FILES&o=LABELS&'
'o=DETAILED_LABELS' % (number,))
related = self.get('changes/%s/revisions/%s/related' % (
number, data['current_revision']))
return data, related
def simpleQuery(self, query, event=None):
def queryChange(self, number, event=None):
if self.session:
data, related = self.queryChangeHTTP(number, event=event)
return GerritChangeData(GerritChangeData.HTTP, data, related)
else:
data = self.queryChangeSSH(number, event=event)
return GerritChangeData(GerritChangeData.SSH, data)
def simpleQuerySSH(self, query, event=None):
def _query_chunk(query, event):
args = '--commit-message --current-patch-set'
@ -1140,9 +1131,63 @@ class GerritConnection(BaseConnection):
"%s %s" % (query, resume), event)
return alldata
def simpleQueryHTTP(self, query, event=None):
iolog = get_annotated_logger(self.iolog, event)
changes = []
sortkey = ''
done = False
offset = 0
while not done:
# We don't actually want to limit to 500, but that's the
# server-side default, and if we don't specify this, we
# won't get a _more_changes flag.
q = ('changes/?n=500%s&o=CURRENT_REVISION&o=CURRENT_COMMIT&'
'q=%s' % (sortkey, query))
iolog.debug('Query: %s', q)
batch = self.get(q)
iolog.debug("Received data from Gerrit query: \n%s",
pprint.pformat(batch))
done = True
if batch:
changes += batch
if '_more_changes' in batch[-1]:
done = False
if '_sortkey' in batch[-1]:
sortkey = '&N=%s' % (batch[-1]['_sortkey'],)
else:
offset += len(batch)
sortkey = '&start=%s' % (offset,)
return changes
def simpleQuery(self, query, event=None):
if self.session:
# None of the users of this method require dependency
# data, so we only perform the change query and omit the
# related changes query.
alldata = self.simpleQueryHTTP(query, event=event)
return [GerritChangeData(GerritChangeData.HTTP, data)
for data in alldata]
else:
alldata = self.simpleQuerySSH(query, event=event)
return [GerritChangeData(GerritChangeData.SSH, data)
for data in alldata]
def _uploadPack(self, project: Project) -> str:
cmd = "git-upload-pack %s" % project.name
out, err = self._ssh(cmd, "0000")
if self.session:
url = ('%s/%s/info/refs?service=git-upload-pack' %
(self.baseurl, project.name))
r = self.session.get(
url,
verify=self.verify_ssl,
auth=self.auth, timeout=TIMEOUT,
headers={'User-Agent': self.user_agent})
self.iolog.debug('Received: %s %s' % (r.status_code, r.text,))
if r.status_code != 200:
raise Exception("Received response %s" % (r.status_code,))
out = r.text[r.text.find('\n') + 5:]
else:
cmd = "git-upload-pack %s" % project.name
out, err = self._ssh(cmd, "0000")
return out
def _open(self):
@ -1231,8 +1276,14 @@ class GerritConnection(BaseConnection):
return ret
def getGitUrl(self, project: Project) -> str:
url = 'ssh://%s@%s:%s/%s' % (self.user, self.server, self.port,
project.name)
if self.session:
baseurl = list(urllib.parse.urlparse(self.baseurl))
baseurl[1] = '%s:%s@%s' % (self.user, self.password, baseurl[1])
baseurl = urllib.parse.urlunparse(baseurl)
url = ('%s/%s' % (baseurl, project.name))
else:
url = 'ssh://%s@%s:%s/%s' % (self.user, self.server, self.port,
project.name)
return url
def _getWebUrl(self, project: Project, sha: str=None) -> str:
@ -1243,7 +1294,8 @@ class GerritConnection(BaseConnection):
def onLoad(self):
self.log.debug("Starting Gerrit Connection/Watchers")
self._start_watcher_thread()
if self.enable_stream_events:
self._start_watcher_thread()
self._start_poller_thread()
self._start_event_connector()

View File

@ -15,10 +15,14 @@
import copy
import re
import time
import urllib.parse
import dateutil.parser
from zuul.model import EventFilter, RefFilter
from zuul.model import Change, TriggerEvent
from zuul.driver.util import time_to_seconds
from zuul import exceptions
EMPTY_GIT_REF = '0' * 40 # git sha of all zeros, used during creates/deletes
@ -29,6 +33,121 @@ class GerritChange(Change):
super(GerritChange, self).__init__(project)
self.approvals = []
def update(self, data, connection):
if data.format == data.SSH:
self.updateFromSSH(data.data, connection)
else:
self.updateFromHTTP(data.data, connection)
def updateFromSSH(self, data, connection):
if self.patchset is None:
self.patchset = str(data['currentPatchSet']['number'])
if 'project' not in data:
raise exceptions.ChangeNotFound(self.number, self.patchset)
self.project = connection.source.getProject(data['project'])
self.id = data['id']
self.branch = data['branch']
self.url = data['url']
urlparse = urllib.parse.urlparse(connection.baseurl)
baseurl = "%s%s" % (urlparse.netloc, urlparse.path)
baseurl = baseurl.rstrip('/')
self.uris = [
'%s/%s' % (baseurl, self.number),
'%s/#/c/%s' % (baseurl, self.number),
'%s/c/%s/+/%s' % (baseurl, self.project.name, self.number),
]
max_ps = 0
files = []
for ps in data['patchSets']:
if str(ps['number']) == self.patchset:
self.ref = ps['ref']
self.commit = ps['revision']
for f in ps.get('files', []):
files.append(f['file'])
if int(ps['number']) > int(max_ps):
max_ps = str(ps['number'])
if max_ps == self.patchset:
self.is_current_patchset = True
else:
self.is_current_patchset = False
self.files = files
self.is_merged = data.get('status', '') == 'MERGED'
self.approvals = data['currentPatchSet'].get('approvals', [])
self.open = data['open']
self.status = data['status']
self.owner = data['owner']
self.message = data['commitMessage']
self.missing_labels = set()
for sr in data.get('submitRecords', []):
if sr['status'] == 'NOT_READY':
for label in sr['labels']:
if label['status'] in ['OK', 'MAY']:
continue
elif label['status'] in ['NEED', 'REJECT']:
self.missing_labels.add(label['label'])
def updateFromHTTP(self, data, connection):
urlparse = urllib.parse.urlparse(connection.baseurl)
baseurl = "%s%s" % (urlparse.netloc, urlparse.path)
baseurl = baseurl.rstrip('/')
current_revision = data['revisions'][data['current_revision']]
if self.patchset is None:
self.patchset = str(current_revision['_number'])
self.project = connection.source.getProject(data['project'])
self.id = data['change_id']
self.branch = data['branch']
self.url = '%s/%s' % (baseurl, self.number)
self.uris = [
'%s/%s' % (baseurl, self.number),
'%s/#/c/%s' % (baseurl, self.number),
'%s/c/%s/+/%s' % (baseurl, self.project.name, self.number),
]
files = []
if str(current_revision['_number']) == self.patchset:
self.ref = current_revision['ref']
self.commit = data['current_revision']
files = current_revision.get('files', []).keys()
self.is_current_patchset = True
else:
self.is_current_patchset = False
self.files = files
self.is_merged = data['status'] == 'MERGED'
self.approvals = []
self.missing_labels = set()
for label_name, label_data in data.get('labels', {}).items():
for app in label_data.get('all', []):
if app.get('value', 0) == 0:
continue
by = {}
for k in ('name', 'username', 'email'):
if k in app:
by[k] = app[k]
self.approvals.append({
"type": label_name,
"description": label_name,
"value": app['value'],
"grantedOn":
dateutil.parser.parse(app['date']).timestamp(),
"by": by,
})
if label_data.get('optional', False):
continue
if label_data.get('blocking', False):
self.missing_labels.add(label_name)
continue
if 'approved' in label_data:
continue
self.missing_labels.add(label_name)
self.open = data['status'] == 'NEW'
self.status = data['status']
self.owner = data['owner']
self.message = current_revision['commit']['message']
class GerritTriggerEvent(TriggerEvent):
"""Incoming event from an external system."""

View File

@ -75,7 +75,7 @@ class GerritSource(BaseSource):
if not results:
return None
change = self.connection._getChange(
results[0]['number'], results[0]['currentPatchSet']['number'])
results[0].number, results[0].current_patchset)
return change
def getChangesDependingOn(self, change, projects, tenant):
@ -89,7 +89,7 @@ class GerritSource(BaseSource):
results = self.connection.simpleQuery(query)
seen = set()
for result in results:
for match in find_dependency_headers(result['commitMessage']):
for match in find_dependency_headers(result.message):
found = False
for uri in change.uris:
if uri in match:
@ -97,13 +97,12 @@ class GerritSource(BaseSource):
break
if not found:
continue
key = (str(result['number']),
str(result['currentPatchSet']['number']))
key = (result.number, result.current_patchset)
if key in seen:
continue
seen.add(key)
change = self.connection._getChange(
result['number'], result['currentPatchSet']['number'])
result.number, result.current_patchset)
changes.append(change)
return changes