/tortoisehg/hgqt/pbranch.py

https://bitbucket.org/tortoisehg/hgtk/ · Python · 606 lines · 524 code · 38 blank · 44 comment · 27 complexity · eec69fd8300486d580857b9a8ca87d7e MD5 · raw file

  1. # pbranch.py - TortoiseHg's patch branch widget
  2. #
  3. # Copyright 2010 Peer Sommerlund <peso@users.sourceforge.net>
  4. #
  5. # This software may be used and distributed according to the terms of the
  6. # GNU General Public License version 2 or any later version.
  7. import os
  8. import time
  9. from mercurial import extensions, ui
  10. from tortoisehg.hgqt.i18n import _
  11. from tortoisehg.hgqt import qtlib, cmdui
  12. from tortoisehg.util import hglib
  13. from PyQt4.QtCore import *
  14. from PyQt4.QtGui import *
  15. nullvariant = QVariant()
  16. class PatchBranchWidget(QWidget):
  17. '''
  18. A widget that show the patch graph and provide actions
  19. for the pbranch extension
  20. '''
  21. output = pyqtSignal(QString, QString)
  22. progress = pyqtSignal(QString, object, QString, QString, object)
  23. makeLogVisible = pyqtSignal(bool)
  24. def __init__(self, repo, parent=None, logwidget=None):
  25. QWidget.__init__(self, parent)
  26. # Set up variables and connect signals
  27. self.repo = repo
  28. self.pbranch = extensions.find('pbranch') # Unfortunately global instead of repo-specific
  29. self.show_internal_branches = False
  30. repo.configChanged.connect(self.configChanged)
  31. repo.repositoryChanged.connect(self.repositoryChanged)
  32. repo.workingBranchChanged.connect(self.workingBranchChanged)
  33. # Build child widgets
  34. vbox = QVBoxLayout()
  35. vbox.setContentsMargins(0, 0, 0, 0)
  36. self.setLayout(vbox)
  37. # Toolbar
  38. self.toolBar_patchbranch = tb = QToolBar(_("Patch Branch Toolbar"), self)
  39. tb.setEnabled(True)
  40. tb.setObjectName("toolBar_patchbranch")
  41. tb.setFloatable(False)
  42. self.actionPMerge = a = QWidgetAction(self)
  43. a.setIcon(QIcon(QPixmap(":/icons/merge.svg")))
  44. a.setToolTip(_('Merge all pending dependencies'))
  45. tb.addAction(self.actionPMerge)
  46. self.actionPMerge.triggered.connect(self.pmerge_clicked)
  47. self.actionBackport = a = QWidgetAction(self)
  48. a.setIcon(QIcon(QPixmap(":/icons/back.svg")))
  49. a.setToolTip(_('Backout current patch branch'))
  50. tb.addAction(self.actionBackport)
  51. #self.actionBackport.triggered.connect(self.pbackout_clicked)
  52. self.actionReapply = a = QWidgetAction(self)
  53. a.setIcon(QIcon(QPixmap(":/icons/forward.svg")))
  54. a.setToolTip(_('Backport part of a changeset to a dependency'))
  55. tb.addAction(self.actionReapply)
  56. #self.actionReapply.triggered.connect(self.reapply_clicked)
  57. self.actionPNew = a = QWidgetAction(self)
  58. a.setIcon(QIcon(QPixmap(":/icons/fileadd.ico"))) #STOCK_NEW
  59. a.setToolTip(_('Start a new patch branch'))
  60. tb.addAction(self.actionPNew)
  61. self.actionPNew.triggered.connect(self.pnew_clicked)
  62. self.actionEditPGraph = a = QWidgetAction(self)
  63. a.setIcon(QIcon(QPixmap(":/icons/log.svg"))) #STOCK_EDIT
  64. a.setToolTip(_('Edit patch dependency graph'))
  65. tb.addAction(self.actionEditPGraph)
  66. #self.actionEditPGraph.triggered.connect(self.pbackout_clicked)
  67. vbox.addWidget(self.toolBar_patchbranch, 1)
  68. # Patch list
  69. self.patchlistmodel = PatchBranchModel(self.compute_model(),
  70. self.repo.changectx('.').branch(),
  71. self)
  72. self.patchlist = QTableView(self)
  73. self.patchlist.setModel(self.patchlistmodel)
  74. self.patchlist.setShowGrid(False)
  75. self.patchlist.verticalHeader().setDefaultSectionSize(20)
  76. self.patchlist.horizontalHeader().setHighlightSections(False)
  77. self.patchlist.setSelectionBehavior(QAbstractItemView.SelectRows)
  78. vbox.addWidget(self.patchlist, 1)
  79. # Command output
  80. self.runner = cmdui.Runner(_('Patch Branch'), True, parent=self)
  81. self.runner.output.connect(self.output)
  82. self.runner.progress.connect(self.progress)
  83. self.runner.makeLogVisible.connect(self.makeLogVisible)
  84. self.runner.commandFinished.connect(self.commandFinished)
  85. self.runner.hide()
  86. vbox.addWidget(self.runner)
  87. def reload(self):
  88. 'User has requested a reload'
  89. self.repo.thginvalidate()
  90. self.refresh()
  91. def refresh(self):
  92. """
  93. Refresh the list of patches.
  94. This operation will try to keep selection state.
  95. """
  96. if not self.pbranch:
  97. return
  98. # store selected patch name
  99. selname = None
  100. patchnamecol = 1 # Column used to store patch name
  101. selinxs = self.patchlist.selectedIndexes()
  102. if len(selinxs) > 0:
  103. selrow = selinxs[0].row()
  104. patchnameinx = self.patchlist.model().index(selrow, patchnamecol)
  105. selname = self.patchlist.model().data(patchnameinx)
  106. # compute model data
  107. self.patchlistmodel.setModel(
  108. self.compute_model(),
  109. self.repo.changectx('.').branch() )
  110. # restore patch selection
  111. if selname:
  112. selinxs = self.patchlistmodel.match(
  113. self.patchlistmodel.index(0, patchnamecol),
  114. Qt.DisplayRole,
  115. selname,
  116. flags = Qt.MatchExactly)
  117. if len(selinxs) > 0:
  118. self.patchlist.setCurrentIndex(selinxs[0])
  119. # update UI sensitives
  120. self.update_sensitivity()
  121. #
  122. # Data functions
  123. #
  124. def compute_model(self):
  125. """
  126. Compute content of table, including patch graph and other columns
  127. """
  128. # compute model data
  129. model = []
  130. # Generate patch branch graph from all heads (option --tips)
  131. opts = {'tips': True}
  132. mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts)
  133. graph = mgr.graphforopts(opts)
  134. if not self.show_internal_branches:
  135. graph = mgr.patchonlygraph(graph)
  136. names = None
  137. patch_list = graph.topolist(names)
  138. in_lines = []
  139. if patch_list:
  140. dep_list = [patch_list[0]]
  141. cur_branch = self.repo['.'].branch()
  142. patch_status = {}
  143. for name in patch_list:
  144. patch_status[name] = self.pstatus(name)
  145. for name in patch_list:
  146. parents = graph.deps(name)
  147. # Node properties
  148. if name in dep_list:
  149. node_column = dep_list.index(name)
  150. else:
  151. node_column = len(dep_list)
  152. node_color = patch_status[name] and '#ff0000' or 0
  153. node_status = (name == cur_branch) and 4 or 0
  154. node = PatchGraphNodeAttributes(node_column, node_color, node_status)
  155. # Find next dependency list
  156. my_deps = []
  157. for p in parents:
  158. if p not in dep_list:
  159. my_deps.append(p)
  160. next_dep_list = dep_list[:]
  161. next_dep_list[node_column:node_column+1] = my_deps
  162. # Dependency lines
  163. shift = len(parents) - 1
  164. out_lines = []
  165. for p in parents:
  166. dep_column = next_dep_list.index(p)
  167. color = 0 # black
  168. if patch_status[p]:
  169. color = '#ff0000' # red
  170. style = 0 # solid lines
  171. out_lines.append(GraphLine(node_column, dep_column, color, style))
  172. for line in in_lines:
  173. if line.end_column == node_column:
  174. # Deps to current patch end here
  175. pass
  176. else:
  177. # Find line continuations
  178. dep = dep_list[line.end_column]
  179. dep_column = next_dep_list.index(dep)
  180. out_lines.append(GraphLine(line.end_column, dep_column, line.color, line.style))
  181. stat = patch_status[name] and 'M' or 'C' # patch status
  182. patchname = name
  183. msg = self.pmessage(name) # summary
  184. if msg:
  185. title = msg.split('\n')[0]
  186. else:
  187. title = None
  188. model.append(PatchGraphNode(node, in_lines, out_lines, patchname, stat,
  189. title, msg))
  190. # Loop
  191. in_lines = out_lines
  192. dep_list = next_dep_list
  193. return model
  194. #
  195. # pbranch extension functions
  196. #
  197. def pgraph(self):
  198. """
  199. [pbranch] Execute 'pgraph' command.
  200. :returns: A list of patches and dependencies
  201. """
  202. if self.pbranch is None:
  203. return None
  204. opts = {}
  205. mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts)
  206. return mgr.graphforopts(opts)
  207. def pstatus(self, patch_name):
  208. """
  209. [pbranch] Execute 'pstatus' command.
  210. :param patch_name: Name of patch-branch
  211. :retv: list of status messages. If empty there is no pending merges
  212. """
  213. if self.pbranch is None:
  214. return None
  215. status = []
  216. opts = {}
  217. mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts)
  218. graph = mgr.graphforopts(opts)
  219. heads = self.repo.branchheads(patch_name)
  220. if len(heads) > 1:
  221. status.append(_('needs merge of %i heads\n') % len(heads))
  222. for dep, through in graph.pendingmerges(patch_name):
  223. if through:
  224. status.append(_('needs merge with %s (through %s)\n') %
  225. (dep, ", ".join(through)))
  226. else:
  227. status.append(_('needs merge with %s\n') % dep)
  228. for dep in graph.pendingrebases(patch_name):
  229. status.append(_('needs update of diff base to tip of %s\n') % dep)
  230. return status
  231. def pmessage(self, patch_name):
  232. """
  233. Get patch message
  234. :param patch_name: Name of patch-branch
  235. :retv: Full patch message. If you extract the first line
  236. you will get the patch title. If the repo does not contain
  237. message or patch, the function returns None
  238. """
  239. opts = {}
  240. mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts)
  241. try:
  242. return mgr.patchdesc(patch_name)
  243. except:
  244. return None
  245. def pnew_ui(self):
  246. """
  247. Create new patch.
  248. Propmt user for new patch name. Patch is created
  249. on current branch.
  250. """
  251. parent = None
  252. title = _('TortoiseHg Prompt')
  253. label = _('New Patch Name')
  254. new_name, ok = QInputDialog.getText(self, title, label)
  255. if not ok:
  256. return False
  257. self.pnew(hglib.fromunicode(new_name))
  258. return True
  259. def pnew(self, patch_name):
  260. """
  261. [pbranch] Execute 'pnew' command.
  262. :param patch_name: Name of new patch-branch
  263. """
  264. if self.pbranch is None:
  265. return False
  266. self.repo.incrementBusyCount()
  267. self.pbranch.cmdnew(self.repo.ui, self.repo, patch_name)
  268. self.repo.decrementBusyCount()
  269. return True
  270. def pmerge(self, patch_name=None):
  271. """
  272. [pbranch] Execute 'pmerge' command.
  273. :param patch_name: Merge to this patch-branch
  274. """
  275. if not self.has_patch():
  276. return
  277. cmd = ['pmerge', '--cwd', self.repo.root]
  278. if patch_name:
  279. cmd += [patch_name]
  280. else:
  281. cmd += ['--all']
  282. self.repo.incrementBusyCount()
  283. self.runner.run(cmd)
  284. def has_pbranch(self):
  285. """ return True if pbranch extension can be used """
  286. return self.pbranch is not None
  287. def has_patch(self):
  288. """ return True if pbranch extension is in use on repo """
  289. return self.has_pbranch() and self.pgraph() != []
  290. ### internal functions ###
  291. def update_sensitivity(self):
  292. """ Update the sensitivity of entire UI """
  293. in_pbranch = True #TODO
  294. is_merge = len(self.repo.parents()) > 1
  295. self.actionPMerge.setEnabled(in_pbranch)
  296. self.actionBackport.setEnabled(in_pbranch)
  297. self.actionReapply.setEnabled(True)
  298. self.actionPNew.setEnabled(not is_merge)
  299. self.actionEditPGraph.setEnabled(True)
  300. # Signal handlers
  301. def commandFinished(self, ret):
  302. self.repo.decrementBusyCount()
  303. self.refresh()
  304. def configChanged(self):
  305. pass
  306. def repositoryChanged(self):
  307. self.refresh()
  308. def workingBranchChanged(self):
  309. self.refresh()
  310. def pnew_clicked(self, toolbutton):
  311. self.pnew_ui()
  312. def pmerge_clicked(self):
  313. self.pmerge()
  314. class PatchGraphNode(object):
  315. """
  316. Simple class to encapsulate a node in the patch branch graph.
  317. Does nothing but declaring attributes.
  318. """
  319. def __init__(self, node, in_lines, out_lines, patchname, stat,
  320. title, msg):
  321. """
  322. :node: attributes related to the node
  323. :in_lines: List of lines above node
  324. :out_lines: List of lines below node
  325. :patchname: Patch branch name
  326. :stat: Status of node - does it need updating or not
  327. :title: First line of patch message
  328. :msg: Full patch message
  329. """
  330. self.node = node
  331. self.toplines = in_lines
  332. self.bottomlines = out_lines
  333. # Find rightmost column used
  334. self.cols = max([max(line.start_column,line.end_column) for line in in_lines + out_lines])
  335. self.patchname = patchname
  336. self.status = stat
  337. self.title = title
  338. self.message = msg
  339. self.msg_esc = msg # u''.join(msg) # escaped summary (utf-8)
  340. class PatchGraphNodeAttributes(object):
  341. """
  342. Simple class to encapsulate attributes about a node in the patch branch graph.
  343. Does nothing but declaring attributes.
  344. """
  345. def __init__(self, column, color, status):
  346. self.column = column
  347. self.color = color
  348. self.status = status
  349. class GraphLine(object):
  350. """
  351. Simple class to encapsulate attributes about a line in the patch branch graph.
  352. Does nothing but declaring attributes.
  353. """
  354. def __init__(self, start_column, end_column, color, style):
  355. self.start_column = start_column
  356. self.end_column = end_column
  357. self.color = color
  358. self.style = style
  359. class PatchBranchContext(object):
  360. """
  361. Similar to patchctx in thgrepo, this class simulates a changeset
  362. for a particular patch branch-
  363. """
  364. class PatchBranchModel(QAbstractTableModel):
  365. """
  366. Model used to list patch branches
  367. TODO: Should be extended to list all branches
  368. """
  369. _columns = ('Graph', 'Name', 'Status', 'Title', 'Message',)
  370. def __init__(self, model, wd_branch="", parent=None):
  371. QAbstractTableModel.__init__(self, parent)
  372. self.rowcount = 0
  373. self._columnmap = {'Graph': lambda ctx, gnode: "",
  374. 'Name': lambda ctx, gnode: gnode.patchname,
  375. 'Status': lambda ctx, gnode: gnode.status,
  376. 'Title': lambda ctx, gnode: gnode.title,
  377. 'Message': lambda ctx, gnode: gnode.message
  378. }
  379. self.model = model
  380. self.wd_branch = wd_branch
  381. self.dotradius = 8
  382. self.rowheight = 20
  383. # virtual functions required to subclass QAbstractTableModel
  384. def rowCount(self, parent=None):
  385. return len(self.model)
  386. def columnCount(self, parent=None):
  387. return len(self._columns)
  388. def data(self, index, role=Qt.DisplayRole):
  389. if not index.isValid():
  390. return nullvariant
  391. row = index.row()
  392. column = self._columns[index.column()]
  393. gnode = self.model[row]
  394. ctx = None
  395. #ctx = self.repo.changectx(gnode.rev)
  396. if role == Qt.DisplayRole:
  397. text = self._columnmap[column](ctx, gnode)
  398. if not isinstance(text, (QString, unicode)):
  399. text = hglib.tounicode(text)
  400. return QVariant(text)
  401. elif role == Qt.ForegroundRole:
  402. return gnode.node.color
  403. if ctx.thgpbunappliedpatch():
  404. return QColor(UNAPPLIED_PATCH_COLOR)
  405. if column == 'Name':
  406. return QVariant(QColor(self.namedbranch_color(ctx.branch())))
  407. elif role == Qt.DecorationRole:
  408. if column == 'Graph':
  409. return self.graphctx(ctx, gnode)
  410. return nullvariant
  411. def headerData(self, section, orientation, role):
  412. if orientation == Qt.Horizontal:
  413. if role == Qt.DisplayRole:
  414. return QVariant(self._columns[section])
  415. if role == Qt.TextAlignmentRole:
  416. return QVariant(Qt.AlignLeft)
  417. return nullvariant
  418. # end of functions required to subclass QAbstractTableModel
  419. def setModel(self, model, wd_branch):
  420. self.beginResetModel()
  421. self.model = model
  422. self.wd_branch = wd_branch
  423. self.endResetModel()
  424. def col2x(self, col):
  425. return 2 * self.dotradius * col + self.dotradius/2 + 8
  426. def graphctx(self, ctx, gnode):
  427. """
  428. Return a QPixmap for the patch graph for the current row
  429. :ctx: Data for current row = branch
  430. :gnode: Node in patch branch graph
  431. :returns: QPixmap of pgraph for ctx
  432. """
  433. w = self.col2x(gnode.cols) + 10
  434. h = self.rowheight
  435. dot_y = h / 2
  436. # Prepare painting: Target pixmap, blue and black pen
  437. pix = QPixmap(w, h)
  438. pix.fill(QColor(0,0,0,0))
  439. painter = QPainter(pix)
  440. painter.setRenderHint(QPainter.Antialiasing)
  441. pen = QPen(Qt.blue)
  442. pen.setWidth(2)
  443. painter.setPen(pen)
  444. lpen = QPen(pen)
  445. lpen.setColor(Qt.black)
  446. painter.setPen(lpen)
  447. # Draw lines
  448. for y1, y4, lines in ((dot_y, dot_y + h, gnode.bottomlines),
  449. (dot_y - h, dot_y, gnode.toplines)):
  450. y2 = y1 + 1 * (y4 - y1)/4
  451. ymid = (y1 + y4)/2
  452. y3 = y1 + 3 * (y4 - y1)/4
  453. for line in lines:
  454. start = line.start_column
  455. end = line.end_column
  456. color = line.color
  457. lpen = QPen(pen)
  458. lpen.setColor(QColor(color))
  459. lpen.setWidth(2)
  460. painter.setPen(lpen)
  461. x1 = self.col2x(start)
  462. x2 = self.col2x(end)
  463. path = QPainterPath()
  464. path.moveTo(x1, y1)
  465. path.cubicTo(x1, y2,
  466. x1, y2,
  467. (x1 + x2)/2, ymid)
  468. path.cubicTo(x2, y3,
  469. x2, y3,
  470. x2, y4)
  471. painter.drawPath(path)
  472. # Draw node
  473. dot_color = QColor(gnode.node.color)
  474. dotcolor = dot_color.lighter()
  475. pencolor = dot_color.darker()
  476. white = QColor("white")
  477. fillcolor = dotcolor #gnode.rev is None and white or dotcolor
  478. pen = QPen(pencolor)
  479. pen.setWidthF(1.5)
  480. painter.setPen(pen)
  481. radius = self.dotradius
  482. centre_x = self.col2x(gnode.node.column)
  483. centre_y = h/2
  484. def circle(r):
  485. rect = QRectF(centre_x - r,
  486. centre_y - r,
  487. 2 * r, 2 * r)
  488. painter.drawEllipse(rect)
  489. def diamond(r):
  490. poly = QPolygonF([QPointF(centre_x - r, centre_y),
  491. QPointF(centre_x, centre_y - r),
  492. QPointF(centre_x + r, centre_y),
  493. QPointF(centre_x, centre_y + r),
  494. QPointF(centre_x - r, centre_y),])
  495. painter.drawPolygon(poly)
  496. if False and ctx.thg_patchbranch(): # diamonds for patches
  497. if ctx.thg_wdbranch():
  498. painter.setBrush(white)
  499. diamond(2 * 0.9 * radius / 1.5)
  500. painter.setBrush(fillcolor)
  501. diamond(radius / 1.5)
  502. else: # circles for normal branches
  503. if gnode.patchname == self.wd_branch:
  504. painter.setBrush(white)
  505. circle(0.9 * radius)
  506. painter.setBrush(fillcolor)
  507. circle(0.5 * radius)
  508. painter.end()
  509. return QVariant(pix)