Add support for cherry-picking to a branch

Change-Id: I50fc591226f15505daf1bb5d954be4a9e00a39b5
This commit is contained in:
James E. Blair 2014-08-31 14:57:25 -07:00
parent 01b575d702
commit de17a501e7
4 changed files with 167 additions and 4 deletions

View File

@ -300,6 +300,15 @@ class Revision(object):
session.flush() session.flush()
return c 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): def getPendingMessage(self):
for m in self.messages: for m in self.messages:
if m.pending: if m.pending:
@ -366,7 +375,7 @@ class PendingCherryPick(object):
mapper(Account, account_table) mapper(Account, account_table)
mapper(Project, project_table, properties=dict( mapper(Project, project_table, properties=dict(
branches=relationship(Branch, backref='project', branches=relationship(Branch, backref='project',
order_by=branch_table.c.key), order_by=branch_table.c.name),
changes=relationship(Change, backref='project', changes=relationship(Change, backref='project',
order_by=change_table.c.number), order_by=change_table.c.number),
unreviewed_changes=relationship(Change, unreviewed_changes=relationship(Change,
@ -415,6 +424,7 @@ mapper(Revision, revision_table, properties=dict(
comment_table.c.draft==True), comment_table.c.draft==True),
order_by=(comment_table.c.line, order_by=(comment_table.c.line,
comment_table.c.created)), comment_table.c.created)),
pending_cherry_picks=relationship(PendingCherryPick, backref='revision'),
)) ))
mapper(Message, message_table, properties=dict( mapper(Message, message_table, properties=dict(
author=relationship(Account))) author=relationship(Account)))
@ -424,8 +434,7 @@ mapper(Label, label_table)
mapper(PermittedLabel, permitted_label_table) mapper(PermittedLabel, permitted_label_table)
mapper(Approval, approval_table, properties=dict( mapper(Approval, approval_table, properties=dict(
reviewer=relationship(Account))) reviewer=relationship(Account)))
mapper(PendingCherryPick, pending_cherry_pick_table, properties=dict( mapper(PendingCherryPick, pending_cherry_pick_table)
revision=relationship(Revision)))
class Database(object): class Database(object):
def __init__(self, app): def __init__(self, app):
@ -539,6 +548,12 @@ class DatabaseSession(object):
except sqlalchemy.orm.exc.NoResultFound: except sqlalchemy.orm.exc.NoResultFound:
return None 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): def getChanges(self, query, unreviewed=False):
self.database.log.debug("Search query: %s" % query) self.database.log.debug("Search query: %s" % query)
q = self.session().query(Change).filter(self.search.parse(query)) q = self.session().query(Change).filter(self.search.parse(query))
@ -611,6 +626,9 @@ class DatabaseSession(object):
def getPendingStatusChanges(self): def getPendingStatusChanges(self):
return self.session().query(Change).filter_by(pending_status=True).all() 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): def getAccountByID(self, id, name=None, username=None, email=None):
try: try:
account = self.session().query(Account).filter_by(id=id).one() account = self.session().query(Account).filter_by(id=id).one()

View File

@ -48,6 +48,7 @@ TOGGLE_HIDDEN_COMMENTS = 'toggle hidden comments'
ABANDON_CHANGE = 'abandon change' ABANDON_CHANGE = 'abandon change'
RESTORE_CHANGE = 'restore change' RESTORE_CHANGE = 'restore change'
REBASE_CHANGE = 'rebase change' REBASE_CHANGE = 'rebase change'
CHERRY_PICK_CHANGE = 'cherry-pick change'
REFRESH = 'refresh' REFRESH = 'refresh'
EDIT_TOPIC = 'edit topic' EDIT_TOPIC = 'edit topic'
# Project list screen: # Project list screen:
@ -89,6 +90,7 @@ DEFAULT_KEYMAP = {
ABANDON_CHANGE: 'ctrl a', ABANDON_CHANGE: 'ctrl a',
RESTORE_CHANGE: 'ctrl e', RESTORE_CHANGE: 'ctrl e',
REBASE_CHANGE: 'ctrl b', REBASE_CHANGE: 'ctrl b',
CHERRY_PICK_CHANGE: 'ctrl x',
REFRESH: 'ctrl r', REFRESH: 'ctrl r',
EDIT_TOPIC: 'ctrl t', EDIT_TOPIC: 'ctrl t',

View File

@ -17,7 +17,9 @@ import collections
import logging import logging
import math import math
import os import os
import re
import threading import threading
import urllib
import urlparse import urlparse
import json import json
import time import time
@ -125,6 +127,49 @@ class SyncProjectListTask(Task):
p = remote[name] p = remote[name]
session.createProject(name, description=p.get('description', '')) session.createProject(name, description=p.get('description', ''))
class SyncSubscribedProjectBranchesTask(Task):
def __repr__(self):
return '<SyncSubscribedProjectBranchesTask>'
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 '<SyncProjectBranchesTask %s>' % (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): class SyncSubscribedProjectsTask(Task):
def __repr__(self): def __repr__(self):
return '<SyncSubscribedProjectsTask>' return '<SyncSubscribedProjectsTask>'
@ -537,6 +582,8 @@ class UploadReviewsTask(Task):
sync.submitTask(RebaseChangeTask(c.key, self.priority)) sync.submitTask(RebaseChangeTask(c.key, self.priority))
for c in session.getPendingStatusChanges(): for c in session.getPendingStatusChanges():
sync.submitTask(ChangeStatusTask(c.key, self.priority)) sync.submitTask(ChangeStatusTask(c.key, self.priority))
for c in session.getPendingCherryPicks():
sync.submitTask(SendCherryPickTask(c.key, self.priority))
for m in session.getPendingMessages(): for m in session.getPendingMessages():
sync.submitTask(UploadReviewTask(m.key, self.priority)) sync.submitTask(UploadReviewTask(m.key, self.priority))
@ -603,6 +650,28 @@ class ChangeStatusTask(Task):
data) data)
sync.submitTask(SyncChangeTask(change.id, priority=self.priority)) 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 '<SendCherryPickTask %s>' % (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): class UploadReviewTask(Task):
def __init__(self, message_key, priority=NORMAL_PRIORITY): def __init__(self, message_key, priority=NORMAL_PRIORITY):
super(UploadReviewTask, self).__init__(priority) super(UploadReviewTask, self).__init__(priority)
@ -662,6 +731,7 @@ class Sync(object):
self.submitTask(UploadReviewsTask(HIGH_PRIORITY)) self.submitTask(UploadReviewsTask(HIGH_PRIORITY))
self.submitTask(SyncProjectListTask(HIGH_PRIORITY)) self.submitTask(SyncProjectListTask(HIGH_PRIORITY))
self.submitTask(SyncSubscribedProjectsTask(HIGH_PRIORITY)) self.submitTask(SyncSubscribedProjectsTask(HIGH_PRIORITY))
self.submitTask(SyncSubscribedProjectBranchesTask(LOW_PRIORITY))
self.periodic_thread = threading.Thread(target=self.periodicSync) self.periodic_thread = threading.Thread(target=self.periodicSync)
self.periodic_thread.daemon = True self.periodic_thread.daemon = True
self.periodic_thread.start() self.periodic_thread.start()
@ -735,6 +805,14 @@ class Sync(object):
headers = {'Content-Type': 'application/json;charset=UTF-8', headers = {'Content-Type': 'application/json;charset=UTF-8',
'User-Agent': self.user_agent}) 'User-Agent': self.user_agent})
self.log.debug('Received: %s' % (r.text,)) 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): def put(self, path, data):
url = self.url(path) url = self.url(path)

View File

@ -65,7 +65,7 @@ class AbandonRestoreDialog(urwid.WidgetWrap):
button_columns = urwid.Columns(button_widgets, dividechars=2) button_columns = urwid.Columns(button_widgets, dividechars=2)
rows = [] rows = []
self.entry = urwid.Edit(edit_text=text, multiline=True) 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(self.entry)
rows.append(urwid.Divider()) rows.append(urwid.Divider())
rows.append(button_columns) rows.append(button_columns)
@ -73,6 +73,36 @@ class AbandonRestoreDialog(urwid.WidgetWrap):
fill = urwid.Filler(pile, valign='top') fill = urwid.Filler(pile, valign='top')
super(AbandonRestoreDialog, self).__init__(urwid.LineBox(fill, '%s Change' % (action,))) 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): class ReviewDialog(urwid.WidgetWrap):
signals = ['save', 'cancel'] signals = ['save', 'cancel']
def __init__(self, revision_row): def __init__(self, revision_row):
@ -378,6 +408,8 @@ class ChangeView(urwid.WidgetWrap):
"Refresh this change"), "Refresh this change"),
(key(keymap.EDIT_TOPIC), (key(keymap.EDIT_TOPIC),
"Edit the topic of this change"), "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(): for k in self.app.config.reviewkeys.values():
@ -762,6 +794,9 @@ class ChangeView(urwid.WidgetWrap):
if keymap.EDIT_TOPIC in commands: if keymap.EDIT_TOPIC in commands:
self.editTopic() self.editTopic()
return None return None
if keymap.CHERRY_PICK_CHANGE in commands:
self.cherryPickChange()
return None
if r in self.app.config.reviewkeys: if r in self.app.config.reviewkeys:
self.reviewKey(self.app.config.reviewkeys[r]) self.reviewKey(self.app.config.reviewkeys[r])
return None return None
@ -819,6 +854,36 @@ class ChangeView(urwid.WidgetWrap):
self.app.backScreen() self.app.backScreen()
self.refresh() 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): def editTopic(self):
dialog = EditTopicDialog(self.app, self.topic) dialog = EditTopicDialog(self.app, self.topic)
urwid.connect_signal(dialog, 'save', urwid.connect_signal(dialog, 'save',