diff --git a/gertty/db.py b/gertty/db.py index 4b230b3..23cfccf 100644 --- a/gertty/db.py +++ b/gertty/db.py @@ -300,6 +300,15 @@ class Revision(object): session.flush() return c + def createPendingCherryPick(self, *args, **kw): + session = Session.object_session(self) + args = [self] + list(args) + c = PendingCherryPick(*args, **kw) + self.pending_cherry_picks.append(c) + session.add(c) + session.flush() + return c + def getPendingMessage(self): for m in self.messages: if m.pending: @@ -366,7 +375,7 @@ class PendingCherryPick(object): mapper(Account, account_table) mapper(Project, project_table, properties=dict( branches=relationship(Branch, backref='project', - order_by=branch_table.c.key), + order_by=branch_table.c.name), changes=relationship(Change, backref='project', order_by=change_table.c.number), unreviewed_changes=relationship(Change, @@ -415,6 +424,7 @@ mapper(Revision, revision_table, properties=dict( comment_table.c.draft==True), order_by=(comment_table.c.line, comment_table.c.created)), + pending_cherry_picks=relationship(PendingCherryPick, backref='revision'), )) mapper(Message, message_table, properties=dict( author=relationship(Account))) @@ -424,8 +434,7 @@ mapper(Label, label_table) mapper(PermittedLabel, permitted_label_table) mapper(Approval, approval_table, properties=dict( reviewer=relationship(Account))) -mapper(PendingCherryPick, pending_cherry_pick_table, properties=dict( - revision=relationship(Revision))) +mapper(PendingCherryPick, pending_cherry_pick_table) class Database(object): def __init__(self, app): @@ -539,6 +548,12 @@ class DatabaseSession(object): except sqlalchemy.orm.exc.NoResultFound: return None + def getPendingCherryPick(self, key): + try: + return self.session().query(PendingCherryPick).filter_by(key=key).one() + except sqlalchemy.orm.exc.NoResultFound: + return None + def getChanges(self, query, unreviewed=False): self.database.log.debug("Search query: %s" % query) q = self.session().query(Change).filter(self.search.parse(query)) @@ -611,6 +626,9 @@ class DatabaseSession(object): def getPendingStatusChanges(self): return self.session().query(Change).filter_by(pending_status=True).all() + def getPendingCherryPicks(self): + return self.session().query(PendingCherryPick).all() + def getAccountByID(self, id, name=None, username=None, email=None): try: account = self.session().query(Account).filter_by(id=id).one() diff --git a/gertty/keymap.py b/gertty/keymap.py index 070ba84..4904742 100644 --- a/gertty/keymap.py +++ b/gertty/keymap.py @@ -48,6 +48,7 @@ TOGGLE_HIDDEN_COMMENTS = 'toggle hidden comments' ABANDON_CHANGE = 'abandon change' RESTORE_CHANGE = 'restore change' REBASE_CHANGE = 'rebase change' +CHERRY_PICK_CHANGE = 'cherry-pick change' REFRESH = 'refresh' EDIT_TOPIC = 'edit topic' # Project list screen: @@ -89,6 +90,7 @@ DEFAULT_KEYMAP = { ABANDON_CHANGE: 'ctrl a', RESTORE_CHANGE: 'ctrl e', REBASE_CHANGE: 'ctrl b', + CHERRY_PICK_CHANGE: 'ctrl x', REFRESH: 'ctrl r', EDIT_TOPIC: 'ctrl t', diff --git a/gertty/sync.py b/gertty/sync.py index d5e1b70..98970ab 100644 --- a/gertty/sync.py +++ b/gertty/sync.py @@ -17,7 +17,9 @@ import collections import logging import math import os +import re import threading +import urllib import urlparse import json import time @@ -125,6 +127,49 @@ class SyncProjectListTask(Task): p = remote[name] session.createProject(name, description=p.get('description', '')) +class SyncSubscribedProjectBranchesTask(Task): + def __repr__(self): + return '' + + def run(self, sync): + app = sync.app + with app.db.getSession() as session: + for p in session.getProjects(subscribed=True): + sync.submitTask(SyncProjectBranchesTask(p.name, self.priority)) + +class SyncProjectBranchesTask(Task): + branch_re = re.compile(r'refs/heads/(.*)') + + def __init__(self, project_name, priority=NORMAL_PRIORITY): + super(SyncProjectBranchesTask, self).__init__(priority) + self.project_name = project_name + + def __repr__(self): + return '' % (self.project_name,) + + def run(self, sync): + app = sync.app + remote = sync.get('projects/%s/branches/' % urllib.quote_plus(self.project_name)) + remote_branches = set() + for x in remote: + m = self.branch_re.match(x['ref']) + if m: + remote_branches.add(m.group(1)) + with app.db.getSession() as session: + local = {} + project = session.getProjectByName(self.project_name) + for branch in project.branches: + local[branch.name] = branch + local_branches = set(local.keys()) + + for name in local_branches-remote_branches: + self.log.debug("Delete branch %s from project %s" % (name, project.name)) + session.delete(local[name]) + + for name in remote_branches-local_branches: + self.log.debug("Add branch %s to project %s" % (name, project.name)) + project.createBranch(name) + class SyncSubscribedProjectsTask(Task): def __repr__(self): return '' @@ -537,6 +582,8 @@ class UploadReviewsTask(Task): sync.submitTask(RebaseChangeTask(c.key, self.priority)) for c in session.getPendingStatusChanges(): sync.submitTask(ChangeStatusTask(c.key, self.priority)) + for c in session.getPendingCherryPicks(): + sync.submitTask(SendCherryPickTask(c.key, self.priority)) for m in session.getPendingMessages(): sync.submitTask(UploadReviewTask(m.key, self.priority)) @@ -603,6 +650,28 @@ class ChangeStatusTask(Task): data) sync.submitTask(SyncChangeTask(change.id, priority=self.priority)) +class SendCherryPickTask(Task): + def __init__(self, cp_key, priority=NORMAL_PRIORITY): + super(SendCherryPickTask, self).__init__(priority) + self.cp_key = cp_key + + def __repr__(self): + return '' % (self.cp_key,) + + def run(self, sync): + app = sync.app + with app.db.getSession() as session: + cp = session.getPendingCherryPick(self.cp_key) + data = dict(message=cp.message, + destination=cp.branch) + session.delete(cp) + # Inside db session for rollback + ret = sync.post('changes/%s/revisions/%s/cherrypick' % + (cp.revision.change.id, cp.revision.commit), + data) + if ret and 'id' in ret: + sync.submitTask(SyncChangeTask(ret['id'], priority=self.priority)) + class UploadReviewTask(Task): def __init__(self, message_key, priority=NORMAL_PRIORITY): super(UploadReviewTask, self).__init__(priority) @@ -662,6 +731,7 @@ class Sync(object): self.submitTask(UploadReviewsTask(HIGH_PRIORITY)) self.submitTask(SyncProjectListTask(HIGH_PRIORITY)) self.submitTask(SyncSubscribedProjectsTask(HIGH_PRIORITY)) + self.submitTask(SyncSubscribedProjectBranchesTask(LOW_PRIORITY)) self.periodic_thread = threading.Thread(target=self.periodicSync) self.periodic_thread.daemon = True self.periodic_thread.start() @@ -735,6 +805,14 @@ class Sync(object): headers = {'Content-Type': 'application/json;charset=UTF-8', 'User-Agent': self.user_agent}) self.log.debug('Received: %s' % (r.text,)) + ret = None + if r.text and len(r.text)>4: + try: + ret = json.loads(r.text[4:]) + except Exception: + self.log.exception("Unable to parse result %s from post to %s" % + (r.text, url)) + return ret def put(self, path, data): url = self.url(path) diff --git a/gertty/view/change.py b/gertty/view/change.py index 3e0b1cf..4be7ad5 100644 --- a/gertty/view/change.py +++ b/gertty/view/change.py @@ -65,7 +65,7 @@ class AbandonRestoreDialog(urwid.WidgetWrap): button_columns = urwid.Columns(button_widgets, dividechars=2) rows = [] self.entry = urwid.Edit(edit_text=text, multiline=True) - rows.append(urwid.Text(u"%s message: " % action)) + rows.append(urwid.Text(u"%s message:" % action)) rows.append(self.entry) rows.append(urwid.Divider()) rows.append(button_columns) @@ -73,6 +73,36 @@ class AbandonRestoreDialog(urwid.WidgetWrap): fill = urwid.Filler(pile, valign='top') super(AbandonRestoreDialog, self).__init__(urwid.LineBox(fill, '%s Change' % (action,))) +class CherryPickDialog(urwid.WidgetWrap): + signals = ['save', 'cancel'] + def __init__(self, change): + save_button = mywid.FixedButton('Propose Change') + cancel_button = mywid.FixedButton('Cancel') + urwid.connect_signal(save_button, 'click', + lambda button:self._emit('save')) + urwid.connect_signal(cancel_button, 'click', + lambda button:self._emit('cancel')) + button_widgets = [('pack', save_button), + ('pack', cancel_button)] + button_columns = urwid.Columns(button_widgets, dividechars=2) + rows = [] + self.entry = urwid.Edit(edit_text=change.revisions[-1].message, multiline=True) + self.branch_buttons = [] + rows.append(urwid.Text(u"Branch:")) + for branch in change.project.branches: + b = mywid.FixedRadioButton(self.branch_buttons, branch.name, + state=(branch.name == change.branch)) + rows.append(b) + rows.append(urwid.Divider()) + rows.append(urwid.Text(u"Commit message:")) + rows.append(self.entry) + rows.append(urwid.Divider()) + rows.append(button_columns) + pile = urwid.Pile(rows) + fill = urwid.Filler(pile, valign='top') + super(CherryPickDialog, self).__init__(urwid.LineBox(fill, + 'Propose Change to Branch')) + class ReviewDialog(urwid.WidgetWrap): signals = ['save', 'cancel'] def __init__(self, revision_row): @@ -378,6 +408,8 @@ class ChangeView(urwid.WidgetWrap): "Refresh this change"), (key(keymap.EDIT_TOPIC), "Edit the topic of this change"), + (key(keymap.CHERRY_PICK_CHANGE), + "Propose this change to another branch"), ] for k in self.app.config.reviewkeys.values(): @@ -762,6 +794,9 @@ class ChangeView(urwid.WidgetWrap): if keymap.EDIT_TOPIC in commands: self.editTopic() return None + if keymap.CHERRY_PICK_CHANGE in commands: + self.cherryPickChange() + return None if r in self.app.config.reviewkeys: self.reviewKey(self.app.config.reviewkeys[r]) return None @@ -819,6 +854,36 @@ class ChangeView(urwid.WidgetWrap): self.app.backScreen() self.refresh() + def cherryPickChange(self): + with self.app.db.getSession() as session: + change = session.getChange(self.change_key) + dialog = CherryPickDialog(change) + urwid.connect_signal(dialog, 'cancel', self.app.backScreen) + urwid.connect_signal(dialog, 'save', lambda button: + self.doCherryPickChange(dialog)) + self.app.popup(dialog, + relative_width=50, relative_height=75, + min_width=60, min_height=20) + + + def doCherryPickChange(self, dialog): + cp_key = None + with self.app.db.getSession() as session: + change = session.getChange(self.change_key) + branch = None + for button in dialog.branch_buttons: + if button.state: + branch = button.get_label() + message = dialog.entry.edit_text + self.app.log.debug("Creating pending cherry-pick of %s to %s" % + (change.revisions[-1].commit, branch)) + cp = change.revisions[-1].createPendingCherryPick(branch, message) + cp_key = cp.key + self.app.sync.submitTask( + sync.SendCherryPickTask(cp_key, sync.HIGH_PRIORITY)) + self.app.backScreen() + self.refresh() + def editTopic(self): dialog = EditTopicDialog(self.app, self.topic) urwid.connect_signal(dialog, 'save',