diff --git a/gertty/app.py b/gertty/app.py index 091950c..9027d1e 100644 --- a/gertty/app.py +++ b/gertty/app.py @@ -27,6 +27,7 @@ from gertty import config from gertty import gitrepo from gertty import mywid from gertty import sync +from gertty import search from gertty.view import change_list as view_change_list from gertty.view import project_list as view_project_list from gertty.view import change as view_change @@ -106,6 +107,7 @@ class App(object): level=level) self.log = logging.getLogger('gertty.App') self.log.debug("Starting") + self.search = search.SearchCompiler(self) self.db = db.Database(self) self.sync = sync.Sync(self) @@ -209,14 +211,12 @@ class App(object): def _syncOneChangeFromQuery(self, query): number = changeid = None - if query.startswith("number:"): + if query.startswith("change:"): number = query.split(':')[1].strip() try: number = int(number) - except Exception: - pass - if query.startswith("changeid:"): - changeid = query.split(':')[1].strip() + except ValueError: + changeid = query.split(':')[1].strip() if not (number or changeid): return with self.db.getSession() as session: @@ -243,14 +243,17 @@ class App(object): if change_key is None: raise Exception('Change is not in local database.') - def search(self, query): + def doSearch(self, query): self.log.debug("Search query: %s" % query) try: self._syncOneChangeFromQuery(query) except Exception as e: return self.error(e.message) with self.db.getSession() as session: - changes = session.getChanges(query) + try: + changes = session.getChanges(query) + except gertty.search.SearchSyntaxError as e: + return self.error(e.message) change_key = None if len(changes) == 1: change_key = changes[0].key @@ -261,7 +264,7 @@ class App(object): view = view_change_list.ChangeListView(self, query) self.changeScreen(view) except gertty.view.DisplayError as e: - self.error(e.message) + return self.error(e.message) def searchDialog(self): dialog = SearchDialog() @@ -275,10 +278,10 @@ class App(object): self.backScreen() query = dialog.entry.edit_text try: - query = 'number:%s' % int(query) - except Exception: + query = 'change:%s' % int(query) + except ValueError: pass - self.search(query) + self.doSearch(query) def error(self, message): dialog = mywid.MessageDialog('Error', message) diff --git a/gertty/db.py b/gertty/db.py index cce5154..293d85f 100644 --- a/gertty/db.py +++ b/gertty/db.py @@ -374,6 +374,7 @@ class DatabaseSession(object): def __init__(self, database): self.database = database self.session = database.session + self.search = database.app.search def __enter__(self): self.database.lock.acquire() @@ -443,25 +444,13 @@ class DatabaseSession(object): return None def getChanges(self, query, unreviewed=False): - #TODO(jeblair): use a real parser that supports the full gerrit query syntax - q = self.session().query(Change) - for term in query.split(): - key, data = term.split(':') - if key == 'number': - q = q.filter(change_table.c.number==data) - elif key == 'changeid': - q = q.filter(change_table.c.change_id==data) - elif key == 'project_key': - q = q.filter(change_table.c.project_key==data) - elif key == 'status': - if data == 'open': - q = q.filter(change_table.c.status.notin_(['MERGED', 'ABANDONED'])) - else: - q = q.filter(change_table.c.status==data) + self.database.log.debug("Search query: %s" % query) + search_filter = self.search.parse(query) + q = self.session().query(Change).filter(search_filter).order_by(change_table.c.number) if unreviewed: q = q.filter(change_table.c.hidden==False, change_table.c.reviewed==False) try: - return q.order_by(change_table.c.number).all() + return q.all() except sqlalchemy.orm.exc.NoResultFound: return [] diff --git a/gertty/search/__init__.py b/gertty/search/__init__.py new file mode 100644 index 0000000..da024ab --- /dev/null +++ b/gertty/search/__init__.py @@ -0,0 +1,26 @@ +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from gertty.search import tokenizer, parser + +class SearchSyntaxError(Exception): + pass + +class SearchCompiler(object): + def __init__(self, app): + self.lexer = tokenizer.SearchTokenizer() + self.parser = parser.SearchParser() + + def parse(self, data): + return self.parser.parse(data, lexer=self.lexer) diff --git a/gertty/search/parser.py b/gertty/search/parser.py new file mode 100644 index 0000000..363f40b --- /dev/null +++ b/gertty/search/parser.py @@ -0,0 +1,236 @@ +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime +import re + +import ply.yacc as yacc +from sqlalchemy.sql.expression import and_, or_ + +import gertty.db +import gertty.search + +from tokenizer import tokens + +def SearchParser(): + def p_terms(p): + '''expression : list_expr + | paren_expr + | boolean_expr + | negative_expr + | term''' + p[0] = p[1] + + def p_list_expr(p): + '''list_expr : expression expression''' + p[0] = and_(p[1], p[2]) + + def p_paren_expr(p): + '''paren_expr : LPAREN expression RPAREN''' + p[0] = p[2] + + def p_boolean_expr(p): + '''boolean_expr : expression AND expression + | expression OR expression''' + if p[2] == 'and': + p[0] = and_(p[1], p[3]) + elif p[2] == 'or': + p[0] = or_(p[1], p[3]) + else: + raise SyntaxErro() + + def p_negative_expr(p): + '''negative_expr : NOT expression + | NEG expression''' + p[0] = not p[1] + + def p_term(p): + '''term : age_term + | change_term + | owner_term + | reviewer_term + | commit_term + | project_term + | project_key_term + | branch_term + | topic_term + | ref_term + | label_term + | message_term + | comment_term + | has_term + | is_term + | status_term + | op_term''' + p[0] = p[1] + + def p_string(p): + '''string : SSTRING + | DSTRING + | USTRING''' + p[0] = p[1] + + def p_age_unit(p): + '''age_unit : SECONDS + | MINUTES + | HOURS + | DAYS + | WEEKS + | MONTHS + | YEARS''' + p[0] = p[1] + + def p_age_term(p): + '''age_term : OP_AGE NUMBER age_unit''' + now = datetime.datetime.utcnow() + delta = p[1] + unit = p[2] + if unit == 'minutes': + delta = delta * 60 + elif unit == 'hours': + delta = delta * 60 * 60 + elif unit == 'days': + delta = delta * 60 * 60 * 60 + elif unit == 'weeks': + delta = delta * 60 * 60 * 60 * 7 + elif unit == 'months': + delta = delta * 60 * 60 * 60 * 30 + elif unit == 'years': + delta = delta * 60 * 60 * 60 * 365 + p[0] = gertty.db.change_table.c.updated < (now-delta) + + def p_change_term(p): + '''change_term : OP_CHANGE CHANGE_ID + | OP_CHANGE NUMBER''' + if type(p[2]) == int: + p[0] = gertty.db.change_table.c.number == p[2] + else: + p[0] = gertty.db.change_table.c.change_id == p[2] + + def p_owner_term(p): + '''owner_term : OP_OWNER string''' + p[0] = gertty.db.change_table.c.owner == p[2] + + def p_reviewer_term(p): + '''reviewer_term : OP_REVIEWER string''' + p[0] = gertty.db.approval_table.c.name == p[2] + + def p_commit_term(p): + '''commit_term : OP_COMMIT string''' + p[0] = gertty.db.revision_table.c.commit == p[2] + + def p_project_term(p): + '''project_term : OP_PROJECT string''' + #TODO: support regex + p[0] = gertty.db.project_table.c.name == p[2] + + def p_project_key_term(p): + '''project_key_term : OP_PROJECT_KEY NUMBER''' + p[0] = gertty.db.change_table.c.project_key == p[2] + + def p_branch_term(p): + '''branch_term : OP_BRANCH string''' + #TODO: support regex + p[0] = gertty.db.change_table.c.branch == p[2] + + def p_topic_term(p): + '''topic_term : OP_TOPIC string''' + #TODO: support regex + p[0] = gertty.db.change_table.c.topic == p[2] + + def p_ref_term(p): + '''ref_term : OP_REF string''' + #TODO: support regex + p[0] = gertty.db.change_table.c.branch == p[2][len('refs/heads/'):] + + label_re = re.compile(r'(?P