/tortoisehg/hgtk/thgpbranch.py

https://bitbucket.org/tortoisehg/hgtk/ · Python · 871 lines · 788 code · 43 blank · 40 comment · 42 complexity · f6223616c8d7fab4d883b30b4d21d742 MD5 · raw file

  1. # thgpbranch.py - embeddable widget for the PatchBranch extension
  2. #
  3. # Copyright 2009 Peer Sommerlund <peer.sommerlund@gmail.com>
  4. #
  5. # This software may be used and distributed according to the terms of the
  6. # GNU General Public License version 2, incorporated herein by reference.
  7. import os
  8. import tempfile
  9. import gtk
  10. import gobject
  11. from mercurial import cmdutil, extensions, util
  12. from mercurial import commands as hg
  13. import mercurial.ui
  14. from tortoisehg.util.i18n import _
  15. from tortoisehg.hgtk import hgcmd
  16. from tortoisehg.hgtk import update
  17. from tortoisehg.hgtk import gtklib, dialog
  18. from tortoisehg.hgtk.logview import graphcell
  19. # Patch Branch model enumeration
  20. M_NODE = 0
  21. M_IN_LINES = 1
  22. M_OUT_LINES = 2
  23. M_NAME = 3
  24. M_STATUS = 4
  25. M_TITLE = 5
  26. M_MSG = 6
  27. M_MSGESC = 7
  28. # Patch Branch column enumeration
  29. C_GRAPH = 0
  30. C_STATUS = 1
  31. C_NAME = 2
  32. C_TITLE = 3
  33. C_MSG = 4
  34. class PBranchWidget(gtk.VBox):
  35. __gproperties__ = {
  36. 'graph-column-visible': (gobject.TYPE_BOOLEAN,
  37. 'Graph',
  38. 'Show graph column',
  39. False,
  40. gobject.PARAM_READWRITE),
  41. 'status-column-visible': (gobject.TYPE_BOOLEAN,
  42. 'Status',
  43. 'Show status column',
  44. False,
  45. gobject.PARAM_READWRITE),
  46. 'name-column-visible': (gobject.TYPE_BOOLEAN,
  47. 'Name',
  48. 'Show name column',
  49. False,
  50. gobject.PARAM_READWRITE),
  51. 'title-column-visible': (gobject.TYPE_BOOLEAN,
  52. 'Title',
  53. 'Show title column',
  54. False,
  55. gobject.PARAM_READWRITE),
  56. 'message-column-visible': (gobject.TYPE_BOOLEAN,
  57. 'Title',
  58. 'Show title column',
  59. False,
  60. gobject.PARAM_READWRITE),
  61. 'show-internal-branches': (gobject.TYPE_BOOLEAN,
  62. 'ShowInternalBranches',
  63. "Show internal branches",
  64. False,
  65. gobject.PARAM_READWRITE)
  66. }
  67. __gsignals__ = {
  68. 'repo-invalidated': (gobject.SIGNAL_RUN_FIRST,
  69. gobject.TYPE_NONE,
  70. ()),
  71. 'patch-selected': (gobject.SIGNAL_RUN_FIRST,
  72. gobject.TYPE_NONE,
  73. (int, # revision number for patch head
  74. str)) # patch name
  75. }
  76. def __init__(self, parentwin, repo, statusbar, accelgroup=None, tooltips=None):
  77. gtk.VBox.__init__(self)
  78. self.parent_window = parentwin
  79. self.repo = repo
  80. self.pbranch = extensions.find('pbranch')
  81. self.statusbar = statusbar
  82. # top toolbar
  83. tbar = gtklib.SlimToolbar(tooltips)
  84. ## buttons
  85. self.btn = {}
  86. pmergebtn = tbar.append_button(gtk.STOCK_CONVERT,
  87. _('Merge all pending dependencies'))
  88. pmergebtn.connect('clicked', self.pmerge_clicked)
  89. self.btn['pmerge'] = pmergebtn
  90. pbackoutbtn = tbar.append_button(gtk.STOCK_GO_BACK,
  91. _('Backout current patch branch'))
  92. pbackoutbtn.connect('clicked', self.pbackout_clicked)
  93. self.btn['pbackout'] = pbackoutbtn
  94. reapplybtn = gtk.ToolButton(gtk.STOCK_GO_FORWARD)
  95. reapplybtn = tbar.append_button(gtk.STOCK_GO_FORWARD,
  96. _('Backport part of a changeset to a dependency'))
  97. reapplybtn.connect('clicked', self.reapply_clicked)
  98. self.btn['reapply'] = reapplybtn
  99. pnewbtn = tbar.append_button(gtk.STOCK_NEW,
  100. _('Start a new patch branch'))
  101. pnewbtn.connect('clicked', self.pnew_clicked)
  102. self.btn['pnew'] = pnewbtn
  103. pgraphbtn = tbar.append_button(gtk.STOCK_EDIT,
  104. _('Edit patch dependency graph'))
  105. pgraphbtn.connect('clicked', self.edit_pgraph_clicked)
  106. self.btn['pnew'] = pnewbtn
  107. ## separator
  108. tbar.append_space()
  109. ## drop-down menu
  110. menubtn = gtk.MenuToolButton('')
  111. menubtn.set_menu(self.create_view_menu())
  112. tbar.append_widget(menubtn, padding=0)
  113. self.btn['menu'] = menubtn
  114. def after_init():
  115. menubtn.child.get_children()[0].hide()
  116. gtklib.idle_add_single_call(after_init)
  117. self.pack_start(tbar, False, False)
  118. # center pane
  119. mainbox = gtk.VBox()
  120. self.pack_start(mainbox, True, True)
  121. ## scrolled pane
  122. pane = gtk.ScrolledWindow()
  123. pane.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
  124. pane.set_shadow_type(gtk.SHADOW_IN)
  125. mainbox.pack_start(pane)
  126. ### patch list
  127. #### patch list model
  128. self.model = gtk.ListStore(
  129. gobject.TYPE_PYOBJECT, # node info
  130. gobject.TYPE_PYOBJECT, # in-lines
  131. gobject.TYPE_PYOBJECT, # out-lines
  132. str, # patch name
  133. str, # patch status
  134. str, # patch title
  135. str, # patch message
  136. str) # patch message escaped
  137. #### patch list view
  138. self.list = gtk.TreeView(self.model)
  139. # To support old PyGTK (<2.12)
  140. if hasattr(self.list, 'set_tooltip_column'):
  141. self.list.set_tooltip_column(M_MSGESC)
  142. self.list.connect('cursor-changed', self.list_sel_changed)
  143. self.list.connect('button-press-event', self.list_pressed)
  144. self.list.connect('row-activated', self.list_row_activated)
  145. self.list.connect('size-allocate', self.list_size_allocated)
  146. #### patch list columns
  147. self.cols = {}
  148. self.cells = {}
  149. def addcol(header, col_idx, model_idx=None, right=False, resizable=False,
  150. editable=False, editfunc=None, cell_renderer=None,
  151. properties=[]):
  152. header = (right and '%s ' or ' %s') % header
  153. cell = cell_renderer or gtk.CellRendererText()
  154. if editfunc:
  155. cell.set_property('editable', editable)
  156. cell.connect('edited', editfunc)
  157. col = gtk.TreeViewColumn(header, cell)
  158. if cell_renderer is None:
  159. col.add_attribute(cell, 'text', model_idx)
  160. col.set_resizable(resizable)
  161. col.set_visible(self.get_property(self.col_to_prop(col_idx)))
  162. if right:
  163. col.set_alignment(1)
  164. cell.set_property('xalign', 1)
  165. for (property_name, model_index) in properties:
  166. col.add_attribute(cell, property_name, model_index)
  167. self.list.append_column(col)
  168. self.cols[col_idx] = col
  169. self.cells[col_idx] = cell
  170. def cell_edited(cell, path, newname):
  171. row = self.model[path]
  172. patchname = row[M_NAME]
  173. if newname != patchname:
  174. self.qrename(newname, patch=patchname)
  175. #### patch list columns and cell renderers
  176. addcol(_('Graph'), C_GRAPH, resizable=True,
  177. cell_renderer=graphcell.CellRendererGraph(),
  178. properties=[("node", M_NODE),
  179. ("in-lines",M_IN_LINES),
  180. ("out-lines", M_OUT_LINES)]
  181. )
  182. addcol(_('St'), C_STATUS, M_STATUS)
  183. addcol(_('Name'), C_NAME, M_NAME, editfunc=cell_edited)
  184. addcol(_('Title'), C_TITLE, M_TITLE)
  185. addcol(_('Message'), C_MSG, M_MSG)
  186. pane.add(self.list)
  187. ## command widget
  188. self.cmd = hgcmd.CmdWidget(style=hgcmd.STYLE_COMPACT,
  189. tooltips=tooltips)
  190. mainbox.pack_start(self.cmd, False, False)
  191. # accelerator
  192. if accelgroup:
  193. # TODO
  194. pass
  195. ### public functions ###
  196. def refresh(self):
  197. """
  198. Refresh the list of patches.
  199. This operation will try to keep selection state.
  200. """
  201. if not self.pbranch:
  202. return
  203. # store selected patch name
  204. selname = None
  205. model, paths = self.list.get_selection().get_selected_rows()
  206. if len(paths) > 0:
  207. selname = model[paths[0]][M_NAME]
  208. # compute model data
  209. self.model.clear()
  210. opts = {'tips': True}
  211. mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts)
  212. graph = mgr.graphforopts(opts)
  213. if not self.get_property('show-internal-branches'):
  214. graph = mgr.patchonlygraph(graph)
  215. names = None
  216. patch_list = graph.topolist(names)
  217. in_lines = []
  218. if patch_list:
  219. dep_list = [patch_list[0]]
  220. cur_branch = self.repo['.'].branch()
  221. patch_status = {}
  222. for name in patch_list:
  223. patch_status[name] = self.pstatus(name)
  224. for name in patch_list:
  225. parents = graph.deps(name)
  226. # Node properties
  227. if name in dep_list:
  228. node_column = dep_list.index(name)
  229. else:
  230. node_column = len(dep_list)
  231. node_colour = patch_status[name] and '#ff0000' or 0
  232. node_status = (name == cur_branch) and 4 or 0
  233. node = (node_column, node_colour, node_status)
  234. # Find next dependency list
  235. my_deps = []
  236. for p in parents:
  237. if p not in dep_list:
  238. my_deps.append(p)
  239. next_dep_list = dep_list[:]
  240. next_dep_list[node_column:node_column+1] = my_deps
  241. # Dependency lines
  242. shift = len(parents) - 1
  243. out_lines = []
  244. for p in parents:
  245. dep_column = next_dep_list.index(p)
  246. colour = 0 # black
  247. if patch_status[p]:
  248. colour = '#ff0000' # red
  249. style = 0 # solid lines
  250. out_lines.append((node_column, dep_column, colour, style))
  251. for lines in in_lines:
  252. (start_column, end_column, colour, style) = lines
  253. if end_column == node_column:
  254. # Deps to current patch end here
  255. pass
  256. else:
  257. # Find line continuations
  258. dep = dep_list[end_column]
  259. dep_column = next_dep_list.index(dep)
  260. out_lines.append((end_column, dep_column, colour, style))
  261. stat = patch_status[name] and 'M' or 'C' # patch status
  262. patchname = name
  263. msg = self.pmessage(name) # summary
  264. if msg:
  265. msg_esc = gtklib.markup_escape_text(msg) # escaped summary (utf-8)
  266. title = msg.split('\n')[0]
  267. else:
  268. msg_esc = None
  269. title = None
  270. self.model.append((node, in_lines, out_lines, patchname, stat,
  271. title, msg, msg_esc))
  272. # Loop
  273. in_lines = out_lines
  274. dep_list = next_dep_list
  275. # restore patch selection
  276. if selname:
  277. iter = self.get_iter_by_patchname(selname)
  278. if iter:
  279. self.list.get_selection().select_iter(iter)
  280. # update UI sensitives
  281. self.update_sensitives()
  282. # report status
  283. status_text = ''
  284. idle_text = None
  285. if self.has_patch():
  286. status_text = self.pending_merges() \
  287. and _('pending pmerges') \
  288. or _('no pending pmerges')
  289. self.statusbar.set_text(status_text, 'pbranch')
  290. self.statusbar.set_idle_text(idle_text)
  291. def pgraph(self):
  292. """
  293. [pbranch] Execute 'pgraph' command.
  294. :returns: A list of patches and dependencies
  295. """
  296. if self.pbranch is None:
  297. return None
  298. opts = {}
  299. mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts)
  300. return mgr.graphforopts(opts)
  301. def patch_list(self, opts={}):
  302. """List all patches in pbranch dependency DAG"""
  303. mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts)
  304. graph = mgr.graphforopts(opts)
  305. names = None
  306. return graph.topolist(names)
  307. def pending_merges(self):
  308. """Return True if there are pending pmerge operations"""
  309. for patch in self.patch_list():
  310. if self.pstatus(patch):
  311. return True
  312. return False
  313. def pstatus(self, patch_name):
  314. """
  315. [pbranch] Execute 'pstatus' command.
  316. :param patch_name: Name of patch-branch
  317. :retv: list of status messages. If empty there is no pending merges
  318. """
  319. if self.pbranch is None:
  320. return None
  321. status = []
  322. opts = {}
  323. mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts)
  324. graph = mgr.graphforopts(opts)
  325. heads = self.repo.branchheads(patch_name)
  326. if len(heads) > 1:
  327. status.append(_('needs merge of %i heads\n') % len(heads))
  328. for dep, through in graph.pendingmerges(patch_name):
  329. if through:
  330. status.append(_('needs merge with %s (through %s)\n') %
  331. (dep, ", ".join(through)))
  332. else:
  333. status.append(_('needs merge with %s\n') % dep)
  334. for dep in graph.pendingrebases(patch_name):
  335. status.append(_('needs update of diff base to tip of %s\n') % dep)
  336. return status
  337. def pmessage(self, patch_name):
  338. """
  339. Get patch message
  340. :param patch_name: Name of patch-branch
  341. :retv: Full patch message. If you extract the first line
  342. you will get the patch title. If the repo does not contain
  343. message or patch, the function returns None
  344. """
  345. opts = {}
  346. mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts)
  347. try:
  348. return mgr.patchdesc(patch_name)
  349. except:
  350. return None
  351. def peditmessage(self, patch_name):
  352. """
  353. Edit patch message
  354. :param patch_name: Name of patch-branch
  355. """
  356. if not patch_name in self.patch_list():
  357. return
  358. cmdline = ['hg', 'peditmessage', patch_name]
  359. self.cmd.execute(cmdline, self.cmd_done)
  360. def pdiff(self, patch_name):
  361. """
  362. [pbranch] Execute 'pdiff --tips' command.
  363. :param patch_name: Name of patch-branch
  364. :retv: list of lines of generated patch
  365. """
  366. opts = {}
  367. mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts)
  368. graph = mgr.graphattips()
  369. return graph.diff(patch_name, None, opts)
  370. def pnew_ui(self):
  371. """
  372. Create new patch.
  373. Propmt user for new patch name. Patch is created
  374. on current branch.
  375. """
  376. parent = None
  377. title = _('New Patch Name')
  378. new_name = dialog.entry_dialog(parent, title)
  379. if not new_name:
  380. return False
  381. self.pnew(new_name)
  382. return True
  383. def pnew(self, patch_name):
  384. """
  385. [pbranch] Execute 'pnew' command.
  386. :param patch_name: Name of new patch-branch
  387. """
  388. if self.pbranch is None:
  389. return False
  390. self.pbranch.cmdnew(self.repo.ui, self.repo, patch_name)
  391. self.emit('repo-invalidated')
  392. return True
  393. def pmerge(self, patch_name=None):
  394. """
  395. [pbranch] Execute 'pmerge' command.
  396. :param patch_name: Merge to this patch-branch
  397. """
  398. if not self.has_patch():
  399. return
  400. cmdline = ['hg', 'pmerge']
  401. if patch_name:
  402. cmdline += [patch_name]
  403. else:
  404. cmdline += ['--all']
  405. self.cmd.execute(cmdline, self.cmd_done)
  406. def pbackout(self):
  407. """
  408. [pbranch] Execute 'pbackout' command.
  409. """
  410. assert False
  411. def pfinish(self, patch_name):
  412. """
  413. [pbranch] Execute 'pfinish' command.
  414. The workdir must be clean.
  415. The patch branch dependencies must be merged.
  416. :param patch_name: A patch branch (not an internal branch)
  417. """
  418. # Check preconditions for pfinish
  419. assert self.is_patch(patch_name)
  420. pmerge_status = self.pstatus(patch_name)
  421. if pmerge_status != []:
  422. dialog.error_dialog(self.parent_window,
  423. _('Pending Pmerge'),
  424. _('You cannot finish this patch branch unless you pmerge it first.\n'
  425. 'pmerge will solve the following issues with %(patch)s:\n'
  426. '* %(issuelist)s') %
  427. {'patch': patch_name,
  428. 'issuelist': '* '.join(pmerge_status)}
  429. )
  430. return
  431. if not self.workdir_is_clean():
  432. dialog.error_dialog(self.parent_window,
  433. _('Uncommitted Local Changes'),
  434. _('pfinish uses your working directory for temporary work.\n'
  435. 'Please commit your local changes before issuing pfinish.')
  436. )
  437. return
  438. if hasattr(self.repo, 'mq') and len(self.repo.mq.applied) > 0:
  439. dialog.error_dialog(self.parent_window,
  440. _('Applied MQ patch'),
  441. _('pfinish must be able to commit, but this is not allowed\n'
  442. 'as long as you have MQ patches applied.')
  443. )
  444. return
  445. # Set up environment for mercurial commands
  446. class CmdWidgetUi(mercurial.ui.ui):
  447. def __init__(self, cmdLogWidget):
  448. src = None
  449. super(CmdWidgetUi, self).__init__(src)
  450. self.cmdLogWidget = cmdLogWidget
  451. def write(self, *args, **opts):
  452. for a in args:
  453. self.cmdLogWidget.append(str(a))
  454. def write_err(self, *args, **opts):
  455. for a in args:
  456. self.cmdLogWidget.append(str(a), error=True)
  457. def flush(self):
  458. pass
  459. def prompt(self, msg, choices=None, default="y"):
  460. raise util.Abort("Internal Error: prompt not available")
  461. def promptchoice(self, msg, choices, default=0):
  462. raise util.Abort("Internal Error: promptchoice not available")
  463. def getpass(self, prompt=None, default=None):
  464. raise util.Abort("Internal Error: getpass not available")
  465. repo = self.repo
  466. ui = CmdWidgetUi(self.cmd.log)
  467. old_ui = repo.ui
  468. repo.ui = ui
  469. # Commit patch to dependency
  470. fd, patch_file_name = tempfile.mkstemp(prefix='thg-patch-')
  471. patch_file = os.fdopen(fd, 'w')
  472. patch_file.writelines(self.pdiff(patch_name))
  473. patch_file.close()
  474. upstream_branch = self.pgraph().deps(patch_name)[0]
  475. hg.update(ui, repo, rev=upstream_branch)
  476. hg.import_(ui, repo, patch_file_name, base='', strip=1)
  477. os.unlink(patch_file_name)
  478. # Close patch branch
  479. hg.update(ui, repo, rev=patch_name)
  480. hg.merge(ui, repo, upstream_branch)
  481. msg = _('Patch branch finished')
  482. hg.commit(ui, repo, close_branch=True, message=msg)
  483. # Update GUI
  484. repo.ui = old_ui
  485. self.emit('repo-invalidated')
  486. def has_pbranch(self):
  487. """ return True if pbranch extension can be used """
  488. return self.pbranch is not None
  489. def has_patch(self):
  490. """ return True if pbranch extension is in use on repo """
  491. if not self.has_pbranch():
  492. return False
  493. g = self.pgraph()
  494. return len(self.pbranch.patchonlygraph(g.mgr, g)._nodes) > 0
  495. def is_patch(self, branch_name):
  496. """ return True if branch is a patch. This excludes root branches
  497. and internal diff base branches (for patches with multiple
  498. dependencies. """
  499. return self.has_pbranch() and self.pgraph().ispatch(branch_name)
  500. def cur_branch(self):
  501. """ Return branch that workdir belongs to. """
  502. return self.repo.dirstate.branch()
  503. def workdir_is_clean(self):
  504. """ return True if the working directory is clean """
  505. c = self.repo[None]
  506. return not (c.modified() or c.added() or c.removed())
  507. ### internal functions ###
  508. def get_iter_by_patchname(self, name):
  509. """ return iter has specified patch name """
  510. if name:
  511. for row in self.model:
  512. if row[M_NAME] == name:
  513. return row.iter
  514. return None
  515. def get_path_by_patchname(self, name):
  516. """ return path has specified patch name """
  517. iter = self.get_iter_by_patchname(name)
  518. if iter:
  519. return self.model.get_path(iter)
  520. return None
  521. def update_sensitives(self):
  522. """ Update the sensitives of entire UI """
  523. def disable_pbranchcmd():
  524. for name in ('pbackout', 'pmerge', 'pnew', 'reapply'):
  525. self.btn[name].set_sensitive(False)
  526. if self.pbranch:
  527. self.list.set_sensitive(True)
  528. self.btn['menu'].set_sensitive(True)
  529. in_pbranch = True #TODO
  530. is_merge = len(self.repo.parents()) > 1
  531. self.btn['pmerge'].set_sensitive(in_pbranch)
  532. self.btn['pbackout'].set_sensitive(in_pbranch)
  533. self.btn['pnew'].set_sensitive(not is_merge)
  534. self.btn['reapply'].set_sensitive(True)
  535. else:
  536. self.list.set_sensitive(False)
  537. self.btn['menu'].set_sensitive(False)
  538. disable_pbranchcmd()
  539. def scroll_to_current(self):
  540. """
  541. Scroll to current patch in the patch list.
  542. If the patch is selected, it will do nothing.
  543. """
  544. if self.list.get_selection().count_selected_rows() > 0:
  545. return
  546. curpatch = self.cur_branch()
  547. if not curpatch:
  548. return
  549. path = self.get_path_by_patchname(curpatch)
  550. if path:
  551. self.list.scroll_to_cell(path)
  552. def show_patch_cmenu(self, list, path):
  553. """Context menu for selected patch"""
  554. row = self.model[path]
  555. menu = gtk.Menu()
  556. def append(label, handler=None):
  557. item = gtk.MenuItem(label, True)
  558. item.set_border_width(1)
  559. if handler:
  560. item.connect('activate', handler, row)
  561. menu.append(item)
  562. has_pbranch = self.has_pbranch()
  563. is_current = self.has_patch() and self.cur_branch() == row[M_NAME]
  564. is_patch = self.is_patch(row[M_NAME])
  565. is_internal = self.pbranch.isinternal(row[M_NAME])
  566. is_merge = len(self.repo.branchheads(row[M_NAME])) > 1
  567. if has_pbranch and not is_merge and not is_internal:
  568. append(_('_new'), self.pnew_activated)
  569. if not is_current:
  570. append(_('_goto (update workdir)'), self.goto_activated)
  571. if is_patch:
  572. append(_('_edit message'), self.edit_message_activated)
  573. append(_('_rename'), self.rename_activated)
  574. append(_('_delete'), self.delete_activated)
  575. append(_('_finish'), self.finish_activated)
  576. if len(menu.get_children()) > 0:
  577. menu.show_all()
  578. menu.popup(None, None, None, 0, 0)
  579. def create_view_menu(self):
  580. """Top right menu for selection of columns and
  581. view configuration."""
  582. menu = gtk.Menu()
  583. def append(item=None, handler=None, check=False,
  584. active=False, sep=False):
  585. if sep:
  586. item = gtk.SeparatorMenuItem()
  587. else:
  588. if isinstance(item, str):
  589. if check:
  590. item = gtk.CheckMenuItem(item)
  591. item.set_active(active)
  592. else:
  593. item = gtk.MenuItem(item)
  594. item.set_border_width(1)
  595. if handler:
  596. item.connect('activate', handler)
  597. menu.append(item)
  598. return item
  599. def colappend(label, col_idx, active=True):
  600. def handler(menuitem):
  601. col = self.cols[col_idx]
  602. col.set_visible(menuitem.get_active())
  603. propname = self.col_to_prop(col_idx)
  604. item = append(label, handler, check=True, active=active)
  605. self.vmenu[propname] = item
  606. self.vmenu = {}
  607. colappend(_('Show graph'), C_GRAPH)
  608. colappend(_('Show status'), C_STATUS, active=False)
  609. colappend(_('Show name'), C_NAME)
  610. colappend(_('Show title'), C_TITLE, active=False)
  611. colappend(_('Show message'), C_MSG, active=False)
  612. append(sep=True)
  613. def enable_editable(item):
  614. self.cells[C_NAME].set_property('editable', item.get_active())
  615. item = append(_('Enable editable cells'), enable_editable,
  616. check=True, active=False)
  617. self.vmenu['editable-cell'] = item
  618. item = append(_("Show internal branches"), lambda item: self.refresh(),
  619. check=True, active=False)
  620. self.vmenu['show-internal-branches'] = item
  621. menu.show_all()
  622. return menu
  623. def show_dialog(self, dlg):
  624. """Show modal dialog and block application
  625. See also show_dialog in history.py
  626. """
  627. dlg.set_transient_for(self.parent_window)
  628. dlg.show_all()
  629. dlg.run()
  630. if gtk.pygtk_version < (2, 12, 0):
  631. # Workaround for old PyGTK (< 2.12.0) issue.
  632. # See background of this: f668034aeda3
  633. dlg.set_transient_for(None)
  634. def update_by_row(self, row):
  635. branch = row[M_NAME]
  636. rev = cmdutil.revrange(self.repo, [branch])
  637. parents = [x.node() for x in self.repo.parents()]
  638. dialog = update.UpdateDialog(rev[0])
  639. self.show_dialog(dialog)
  640. self.update_completed(parents)
  641. def update_completed(self, oldparents):
  642. self.repo.invalidate()
  643. self.repo.dirstate.invalidate()
  644. newparents = [x.node() for x in self.repo.parents()]
  645. if not oldparents == newparents:
  646. self.emit('repo-invalidated')
  647. def cmd_done(self, returncode, useraborted, noemit=False):
  648. if returncode == 0:
  649. if self.cmd.get_pbar():
  650. self.cmd.set_result(_('Succeed'), style='ok')
  651. elif useraborted:
  652. self.cmd.set_result(_('Canceled'), style='error')
  653. else:
  654. self.cmd.set_result(_('Failed'), style='error')
  655. self.refresh()
  656. if not noemit:
  657. self.emit('repo-invalidated')
  658. def do_get_property(self, property):
  659. try:
  660. return self.vmenu[property.name].get_active()
  661. except:
  662. raise AttributeError, 'unknown property %s' % property.name
  663. def do_set_property(self, property, value):
  664. try:
  665. self.vmenu[property.name].set_active(value)
  666. except:
  667. raise AttributeError, 'unknown property %s' % property.name
  668. def col_to_prop(self, col_idx):
  669. if col_idx == C_GRAPH:
  670. return 'graph-column-visible'
  671. if col_idx == C_STATUS:
  672. return 'status-column-visible'
  673. if col_idx == C_NAME:
  674. return 'name-column-visible'
  675. if col_idx == C_TITLE:
  676. return 'title-column-visible'
  677. if col_idx == C_MSG:
  678. return 'message-column-visible'
  679. return ''
  680. ### signal handlers ###
  681. def list_pressed(self, list, event):
  682. x, y = int(event.x), int(event.y)
  683. pathinfo = list.get_path_at_pos(x, y)
  684. if event.button == 1:
  685. if not pathinfo:
  686. # HACK: clear selection after this function calling,
  687. # against selection by getting focus
  688. def unselect():
  689. selection = list.get_selection()
  690. selection.unselect_all()
  691. gtklib.idle_add_single_call(unselect)
  692. elif event.button == 3:
  693. if pathinfo:
  694. self.show_patch_cmenu(self.list, pathinfo[0])
  695. def list_sel_changed(self, list):
  696. path, focus = list.get_cursor()
  697. row = self.model[path]
  698. patchname = row[M_NAME]
  699. try:
  700. ctx = self.repo[patchname]
  701. revid = ctx.rev()
  702. except hglib.RepoError:
  703. revid = -1
  704. self.emit('patch-selected', revid, patchname)
  705. def list_row_activated(self, list, path, column):
  706. self.update_by_row(self.model[path])
  707. def list_size_allocated(self, list, req):
  708. if self.has_patch():
  709. self.scroll_to_current()
  710. def pbackout_clicked(self, toolbutton):
  711. pass
  712. def pmerge_clicked(self, toolbutton):
  713. self.pmerge()
  714. def pnew_clicked(self, toolbutton):
  715. self.pnew_ui()
  716. def reapply_clicked(self, toolbutton):
  717. pass
  718. def edit_pgraph_clicked(self, toolbutton):
  719. opts = {} # TODO: How to find user ID
  720. mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts)
  721. oldtext = mgr.graphdesc()
  722. # run editor in the repository root
  723. olddir = os.getcwd()
  724. os.chdir(self.repo.root)
  725. newtext = self.repo.ui.edit(oldtext, opts.get('user'))
  726. os.chdir(olddir)
  727. mgr.updategraphdesc(newtext)
  728. ### context menu signal handlers ###
  729. def pnew_activated(self, menuitem, row):
  730. """Insert new patch after this row"""
  731. if self.cur_branch() == row[M_NAME]:
  732. self.pnew_ui()
  733. return
  734. # pnew from patch different than current
  735. assert False
  736. if self.wdir_modified():
  737. # Ask user if current changes should be discarded
  738. # Abort if user does not agree
  739. pass
  740. # remember prev branch
  741. # Update to row[M_NAME]
  742. # pnew_ui
  743. # if aborted, update back to prev branch
  744. pass
  745. def edit_message_activated(self, menuitem, row):
  746. self.peditmessage(row[M_NAME])
  747. def goto_activated(self, menuitem, row):
  748. self.update_by_row(row)
  749. def delete_activated(self, menuitem, row):
  750. assert False
  751. def rename_activated(self, menuitem, row):
  752. assert False
  753. def finish_activated(self, menuitem, row):
  754. self.pfinish(row[M_NAME])