PageRenderTime 53ms CodeModel.GetById 17ms app.highlight 31ms RepoModel.GetById 1ms app.codeStats 0ms

/tortoisehg/hgqt/pbranch.py

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