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()
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()

View File

@ -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',

View File

@ -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 '<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):
def __repr__(self):
return '<SyncSubscribedProjectsTask>'
@ -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 '<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):
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)

View File

@ -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',