This instance will be upgraded to Heptapod 0.21.0rc2 on 2021-04-16 at 11:00 UTC+2 (a few minutes of down time)

csinfo.py 18.7 KB
Newer Older
Yuki KODAMA's avatar
Yuki KODAMA committed
1 2 3 4 5 6 7
# csinfo.py - An embeddable widget for changeset summary
#
# Copyright 2010 Yuki KODAMA <endflow.net@gmail.com>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2, incorporated herein by reference.

8 9
from __future__ import absolute_import

Yuki KODAMA's avatar
Yuki KODAMA committed
10
import binascii
11
import re
Yuki KODAMA's avatar
Yuki KODAMA committed
12

13 14 15 16 17 18 19 20 21 22
from .qtcore import (
    QSize,
    Qt,
    pyqtSignal,
)
from .qtgui import (
    QHBoxLayout,
    QLabel,
    QWidget,
)
Yuki KODAMA's avatar
Yuki KODAMA committed
23

24 25 26 27
from mercurial import (
    error,
    pycompat,
)
Yuki KODAMA's avatar
Yuki KODAMA committed
28

29 30 31
from ..util import hglib
from ..util.i18n import _
from . import qtlib
Yuki KODAMA's avatar
Yuki KODAMA committed
32

Adrian Buehlmann's avatar
Adrian Buehlmann committed
33
PANEL_DEFAULT = ('rev', 'summary', 'user', 'dateage', 'branch', 'close',
Angel Ezquerra's avatar
Angel Ezquerra committed
34 35
                 'tags', 'graft', 'transplant', 'obsolete',
                 'p4', 'svn', 'converted',)
Yuki KODAMA's avatar
Yuki KODAMA committed
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69

def create(repo, target=None, style=None, custom=None, **kargs):
    return Factory(repo, custom, style, target, **kargs)()

def factory(*args, **kargs):
    return Factory(*args, **kargs)

def panelstyle(**kargs):
    kargs['type'] = 'panel'
    if 'contents' not in kargs:
        kargs['contents'] = PANEL_DEFAULT
    return kargs

def labelstyle(**kargs):
    kargs['type'] = 'label'
    return kargs

def custom(**kargs):
    return kargs

class Factory(object):

    def __init__(self, repo, custom=None, style=None, target=None,
                 withupdate=False):
        if repo is None:
            raise _('must be specified repository')
        self.repo = repo
        self.target = target
        if custom is None:
            custom = {}
        self.custom = custom
        if style is None:
            style = panelstyle()
        self.csstyle = style
Yuki KODAMA's avatar
Yuki KODAMA committed
70
        self.info = SummaryInfo()
Yuki KODAMA's avatar
Yuki KODAMA committed
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99

        self.withupdate = withupdate

    def __call__(self, target=None, style=None, custom=None, repo=None):
        # try to create a context object
        if target is None:
            target = self.target
        if repo is None:
            repo = self.repo

        if style is None:
            style = self.csstyle
        else:
            # need to override styles
            newstyle = self.csstyle.copy()
            newstyle.update(style)
            style = newstyle

        if custom is None:
            custom = self.custom
        else:
            # need to override customs
            newcustom = self.custom.copy()
            newcustom.update(custom)
            custom = newcustom

        if 'type' not in style:
            raise _("must be specified 'type' in style")
        type = style['type']
100
        assert type in ('panel', 'label'), type
Yuki KODAMA's avatar
Yuki KODAMA committed
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118

        # create widget
        args = (target, style, custom, repo, self.info)
        if type == 'panel':
            widget = SummaryPanel(*args)
        else:
            widget = SummaryLabel(*args)
        if self.withupdate:
            widget.update()
        return widget

class UnknownItem(Exception):
    pass


class SummaryInfo(object):

    LABELS = {'rev': _('Revision:'), 'revnum': _('Revision:'),
119
              'gitcommit': _('Git Commit:'),
Yuki KODAMA's avatar
Yuki KODAMA committed
120 121 122
              'revid': _('Revision:'), 'summary': _('Summary:'),
              'user': _('User:'), 'date': _('Date:'),'age': _('Age:'),
              'dateage': _('Date:'), 'branch': _('Branch:'),
Adrian Buehlmann's avatar
Adrian Buehlmann committed
123
              'close': _('Close:'),
Yuki KODAMA's avatar
Yuki KODAMA committed
124
              'tags': _('Tags:'), 'rawbranch': _('Branch:'),
125
              'graft': _('Graft:'),
126
              'transplant': _('Transplant:'),
Angel Ezquerra's avatar
Angel Ezquerra committed
127
              'obsolete': _('Obsolete state:'),
Yuki KODAMA's avatar
Yuki KODAMA committed
128
              'p4': _('Perforce:'), 'svn': _('Subversion:'),
129 130 131
              'converted': _('Converted From:'), 'shortuser': _('User:'),
              'mqoriginalparent': _('Original Parent:')
    }
Yuki KODAMA's avatar
Yuki KODAMA committed
132 133 134 135 136 137 138 139 140 141 142 143 144

    def __init__(self):
        pass

    def get_data(self, item, widget, ctx, custom, **kargs):
        args = (widget, ctx, custom)
        def default_func(widget, item, ctx):
            return None
        def preset_func(widget, item, ctx):
            if item == 'rev':
                revnum = self.get_data('revnum', *args)
                revid = self.get_data('revid', *args)
                if revid:
145
                    return revnum, revid
Yuki KODAMA's avatar
Yuki KODAMA committed
146 147 148 149 150
                return None
            elif item == 'revnum':
                return ctx.rev()
            elif item == 'revid':
                return str(ctx)
151 152
            elif item == 'gitcommit':
                return hglib.gitcommit(ctx)
Yuki KODAMA's avatar
Yuki KODAMA committed
153
            elif item == 'desc':
154
                return hglib.tounicode(ctx.description().replace(b'\0', b''))
Yuki KODAMA's avatar
Yuki KODAMA committed
155
            elif item == 'summary':
156
                summary = hglib.longsummary(
157
                    ctx.description().replace(b'\0', b''))
158
                if len(summary) == 0:
159
                    return None
160
                return summary
Yuki KODAMA's avatar
Yuki KODAMA committed
161
            elif item == 'user':
162 163 164 165
                user = hglib.user(ctx)
                if user:
                    return hglib.tounicode(user)
                return None
Yuki KODAMA's avatar
Yuki KODAMA committed
166
            elif item == 'shortuser':
167
                return hglib.tounicode(hglib.username(hglib.user(ctx)))
Yuki KODAMA's avatar
Yuki KODAMA committed
168 169 170 171
            elif item == 'dateage':
                date = self.get_data('date', *args)
                age = self.get_data('age', *args)
                if date and age:
172
                    return date, age
Yuki KODAMA's avatar
Yuki KODAMA committed
173 174 175 176
                return None
            elif item == 'date':
                date = ctx.date()
                if date:
177
                    return hglib.tounicode(hglib.displaytime(date))
Yuki KODAMA's avatar
Yuki KODAMA committed
178 179 180 181
                return None
            elif item == 'age':
                date = ctx.date()
                if date:
182
                    return hglib.age(date).decode('utf-8')
Yuki KODAMA's avatar
Yuki KODAMA committed
183 184
                return None
            elif item == 'rawbranch':
185
                return hglib.tounicode(ctx.branch()) or None
Yuki KODAMA's avatar
Yuki KODAMA committed
186 187 188 189
            elif item == 'branch':
                value = self.get_data('rawbranch', *args)
                if value:
                    repo = ctx._repo
190 191 192 193 194
                    try:
                        if ctx.node() != repo.branchtip(ctx.branch()):
                            return None
                    except error.RepoLookupError:
                        # ctx.branch() can be invalid for null or workingctx
Yuki KODAMA's avatar
Yuki KODAMA committed
195
                        return None
Steve Borho's avatar
Steve Borho committed
196
                    if value in repo.deadbranches:
Yuki KODAMA's avatar
Yuki KODAMA committed
197 198 199
                        return None
                    return value
                return None
Adrian Buehlmann's avatar
Adrian Buehlmann committed
200
            elif item == 'close':
201
                return hglib.tounicode(ctx.extra().get(b'close'))
Yuki KODAMA's avatar
Yuki KODAMA committed
202
            elif item == 'tags':
203
                return [hglib.tounicode(tag) for tag in ctx.thgtags()] or None
204 205 206
            elif item == 'graft':
                extra = ctx.extra()
                try:
207
                    return hglib.tounicode(extra[b'source'])
208 209 210
                except KeyError:
                    pass
                return None
Yuki KODAMA's avatar
Yuki KODAMA committed
211 212 213
            elif item == 'transplant':
                extra = ctx.extra()
                try:
214
                    ts = extra[b'transplant_source']
Yuki KODAMA's avatar
Yuki KODAMA committed
215
                    if ts:
216
                        return hglib.tounicode(binascii.hexlify(ts))
Yuki KODAMA's avatar
Yuki KODAMA committed
217 218 219
                except KeyError:
                    pass
                return None
Angel Ezquerra's avatar
Angel Ezquerra committed
220 221 222 223 224 225
            elif item == 'obsolete':
                obsoletestate = []
                if ctx.obsolete():
                    obsoletestate.append('obsolete')
                if ctx.extinct():
                    obsoletestate.append('extinct')
226
                obsoletestate += map(hglib.tounicode, ctx.instabilities())
Angel Ezquerra's avatar
Angel Ezquerra committed
227 228 229
                if obsoletestate:
                    return obsoletestate
                return None
Yuki KODAMA's avatar
Yuki KODAMA committed
230 231
            elif item == 'p4':
                extra = ctx.extra()
232 233
                p4cl = extra.get(b'p4', None)
                return p4cl and hglib.tounicode(b'changelist %s' % p4cl)
Yuki KODAMA's avatar
Yuki KODAMA committed
234 235
            elif item == 'svn':
                extra = ctx.extra()
236 237 238
                cvt = extra.get(b'convert_revision', b'')
                if cvt.startswith(b'svn:'):
                    result = cvt.split(b'/', 1)[-1]
Yuki KODAMA's avatar
Yuki KODAMA committed
239
                    if cvt != result:
240 241
                        return hglib.tounicode(result)
                    return hglib.tounicode(cvt.split(b'@')[-1])
Yuki KODAMA's avatar
Yuki KODAMA committed
242 243
                else:
                    return None
244 245
            elif item == 'converted':
                extra = ctx.extra()
246 247 248
                cvt = extra.get(b'convert_revision', b'')
                if cvt and not cvt.startswith(b'svn:'):
                    return hglib.tounicode(cvt)
249 250
                else:
                    return None
Yuki KODAMA's avatar
Yuki KODAMA committed
251
            elif item == 'ishead':
252
                return ctx.node() in ctx.repo().branchheads(ctx.branch())
253 254 255 256 257 258 259 260 261
            elif item == 'mqoriginalparent':
                target = ctx.thgmqoriginalparent()
                if not target:
                    return None
                p1 = ctx.p1()
                if p1 is not None and p1.hex() == target:
                    return None
                if target not in ctx._repo:
                    return None
Sergey Orlov's avatar
Sergey Orlov committed
262
                return hglib.tounicode(target)
Yuki KODAMA's avatar
Yuki KODAMA committed
263
            raise UnknownItem(item)
264
        if 'data' in custom and not kargs.get('usepreset', False):
Yuki KODAMA's avatar
Yuki KODAMA committed
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
            try:
                return custom['data'](widget, item, ctx)
            except UnknownItem:
                pass
        try:
            return preset_func(widget, item, ctx)
        except UnknownItem:
            pass
        return default_func(widget, item, ctx)

    def get_label(self, item, widget, ctx, custom, **kargs):
        def default_func(widget, item):
            return ''
        def preset_func(widget, item):
            try:
                return self.LABELS[item]
            except KeyError:
                raise UnknownItem(item)
283
        if 'label' in custom and not kargs.get('usepreset', False):
Yuki KODAMA's avatar
Yuki KODAMA committed
284
            try:
285
                return custom['label'](widget, item, ctx)
Yuki KODAMA's avatar
Yuki KODAMA committed
286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
            except UnknownItem:
                pass
        try:
            return preset_func(widget, item)
        except UnknownItem:
            pass
        return default_func(widget, item)

    def get_markup(self, item, widget, ctx, custom, **kargs):
        args = (widget, ctx, custom)
        mono = dict(family='monospace', size='9pt', space='pre')
        def default_func(widget, item, value):
            return ''
        def preset_func(widget, item, value):
            if item == 'rev':
                revnum, revid = value
                revid = qtlib.markup(revid, **mono)
                if revnum is not None and revid is not None:
                    return '%s (%s)' % (revnum, revid)
                return '%s' % revid
306 307
            elif item in ('revid', 'gitcommit', 'graft', 'transplant',
                          'mqoriginalparent'):
Yuki KODAMA's avatar
Yuki KODAMA committed
308
                return qtlib.markup(value, **mono)
309
            elif item in ('revnum', 'p4', 'close', 'converted'):
Yuki KODAMA's avatar
Yuki KODAMA committed
310
                return str(value)
311 312
            elif item == 'svn':
                # svn is always in utf-8 because ctx.extra() isn't converted
313
                return pycompat.unicode(value, 'utf-8', 'replace')
Yuki KODAMA's avatar
Yuki KODAMA committed
314 315 316
            elif item in ('rawbranch', 'branch'):
                opts = dict(fg='black', bg='#aaffaa')
                return qtlib.markup(' %s ' % value, **opts)
317
            elif item == 'tags':
Yuki KODAMA's avatar
Yuki KODAMA committed
318 319 320 321 322 323 324 325
                opts = dict(fg='black', bg='#ffffaa')
                tags = [qtlib.markup(' %s ' % tag, **opts) for tag in value]
                return ' '.join(tags)
            elif item in ('desc', 'summary', 'user', 'shortuser',
                          'date', 'age'):
                return qtlib.markup(value)
            elif item == 'dateage':
                return qtlib.markup('%s (%s)' % value)
Angel Ezquerra's avatar
Angel Ezquerra committed
326 327 328 329 330
            elif item == 'obsolete':
                opts = dict(fg='black', bg='#ff8566')
                obsoletestates = [qtlib.markup(' %s ' % state, **opts)
                                  for state in value]
                return ' '.join(obsoletestates)
Yuki KODAMA's avatar
Yuki KODAMA committed
331 332 333 334
            raise UnknownItem(item)
        value = self.get_data(item, *args)
        if value is None:
            return None
335
        if 'markup' in custom and not kargs.get('usepreset', False):
Yuki KODAMA's avatar
Yuki KODAMA committed
336 337 338 339 340 341 342 343 344 345 346 347 348
            try:
                return custom['markup'](widget, item, value)
            except UnknownItem:
                pass
        try:
            return preset_func(widget, item, value)
        except UnknownItem:
            pass
        return default_func(widget, item, value)

    def get_widget(self, item, widget, ctx, custom, **kargs):
        args = (widget, ctx, custom)
        def default_func(widget, item, markups):
349
            if hglib.isbasestring(markups):
Yuki KODAMA's avatar
Yuki KODAMA committed
350 351 352
                markups = (markups,)
            labels = []
            for text in markups:
353 354
                label = QLabel()
                label.setText(text)
Yuki KODAMA's avatar
Yuki KODAMA committed
355 356 357 358 359
                labels.append(label)
            return labels
        markups = self.get_markup(item, *args)
        if not markups:
            return None
360
        if 'widget' in custom and not kargs.get('usepreset', False):
Yuki KODAMA's avatar
Yuki KODAMA committed
361 362 363 364 365 366 367 368 369
            try:
                return custom['widget'](widget, item, markups)
            except UnknownItem:
                pass
        return default_func(widget, item, markups)

class SummaryBase(object):

    def __init__(self, target, custom, repo, info):
370
        self.target = target
Yuki KODAMA's avatar
Yuki KODAMA committed
371 372 373
        self.custom = custom
        self.repo = repo
        self.info = info
374
        self.update_ctx()
Yuki KODAMA's avatar
Yuki KODAMA committed
375 376 377 378 379 380 381 382 383 384 385 386 387

    def get_data(self, item, **kargs):
        return self.info.get_data(item, self, self.ctx, self.custom, **kargs)

    def get_label(self, item, **kargs):
        return self.info.get_label(item, self, self.ctx, self.custom, **kargs)

    def get_markup(self, item, **kargs):
        return self.info.get_markup(item, self, self.ctx, self.custom, **kargs)

    def get_widget(self, item, **kargs):
        return self.info.get_widget(item, self, self.ctx, self.custom, **kargs)

388 389 390
    def set_revision(self, rev):
        self.target = rev

Yuki KODAMA's avatar
Yuki KODAMA committed
391 392 393 394 395 396 397 398 399 400 401
    def update(self, target=None, custom=None, repo=None):
        self.ctx = None
        if target is not None:
            self.target = target
        if custom is not None:
            self.custom = custom
        if repo is None:
            repo = self.repo
        if repo is not None:
            self.repo = repo
        if self.ctx is None:
402 403 404
            self.update_ctx()

    def update_ctx(self):
405 406 407 408 409
        if isinstance(self.target, pycompat.unicode):
            target = hglib.fromunicode(self.target)
        else:
            target = self.target
        self.ctx = self.repo[target]
Yuki KODAMA's avatar
Yuki KODAMA committed
410

Yuki KODAMA's avatar
Yuki KODAMA committed
411 412
PANEL_TMPL = '<tr><td style="padding-right:6px">%s</td><td>%s</td></tr>'

413
class SummaryPanel(SummaryBase, QWidget):
Yuki KODAMA's avatar
Yuki KODAMA committed
414

415
    linkActivated = pyqtSignal(str)
416

Yuki KODAMA's avatar
Yuki KODAMA committed
417 418
    def __init__(self, target, style, custom, repo, info):
        SummaryBase.__init__(self, target, custom, repo, info)
419
        QWidget.__init__(self)
Yuki KODAMA's avatar
Yuki KODAMA committed
420 421 422

        self.csstyle = style

423
        hbox = QHBoxLayout()
424
        hbox.setContentsMargins(0, 0, 0, 0)
425
        hbox.setSpacing(0)
426 427
        self.setLayout(hbox)
        self.revlabel = None
428
        self.expand_btn = qtlib.PMButton()
429

Yuki KODAMA's avatar
Yuki KODAMA committed
430
    def update(self, target=None, style=None, custom=None, repo=None):
431
        SummaryBase.update(self, target, custom, repo)
Yuki KODAMA's avatar
Yuki KODAMA committed
432

433 434 435
        layout = self.layout()
        assert isinstance(layout, QHBoxLayout)

Yuki KODAMA's avatar
Yuki KODAMA committed
436 437 438
        if style is not None:
            self.csstyle = style

439 440
        if self.revlabel is None:
            self.revlabel = QLabel()
441
            self.revlabel.linkActivated.connect(self.linkActivated)
442
            layout.addWidget(self.revlabel, 0, Qt.AlignTop)
443 444

        if 'expandable' in self.csstyle and self.csstyle['expandable']:
445
            if self.expand_btn.parentWidget() is None:
446 447
                self.expand_btn.clicked.connect(lambda: self.update())
                margin = QHBoxLayout()
448
                margin.setContentsMargins(3, 3, 3, 3)
449
                margin.addWidget(self.expand_btn, 0, Qt.AlignTop)
450
                layout.insertLayout(0, margin)
451
            self.expand_btn.setVisible(True)
452
        elif self.expand_btn.parentWidget() is not None:
453
            self.expand_btn.setHidden(True)
454

455 456 457 458 459 460
        interact = Qt.LinksAccessibleByMouse

        if 'selectable' in self.csstyle and self.csstyle['selectable']:
            interact |= Qt.TextBrowserInteraction

        self.revlabel.setTextInteractionFlags(interact)
Yuki KODAMA's avatar
Yuki KODAMA committed
461 462 463

        # build info
        contents = self.csstyle.get('contents', ())
464
        if 'expandable' in self.csstyle and self.csstyle['expandable'] \
465 466
                                        and self.expand_btn.is_collapsed():
            contents = contents[0:1]
Yuki KODAMA's avatar
Yuki KODAMA committed
467

Yuki KODAMA's avatar
Yuki KODAMA committed
468 469
        if 'margin' in self.csstyle:
            margin = self.csstyle['margin']
470
            assert isinstance(margin, (int, pycompat.long)), repr(margin)
Yuki KODAMA's avatar
Yuki KODAMA committed
471 472 473
            buf = '<table style="margin: %spx">' % margin
        else:
            buf = '<table>'
Yuki KODAMA's avatar
Yuki KODAMA committed
474 475

        for item in contents:
Yuki KODAMA's avatar
Yuki KODAMA committed
476 477
            markups = self.get_markup(item)
            if not markups:
Yuki KODAMA's avatar
Yuki KODAMA committed
478
                continue
Yuki KODAMA's avatar
Yuki KODAMA committed
479
            label = qtlib.markup(self.get_label(item), weight='bold')
480
            if hglib.isbasestring(markups):
Yuki KODAMA's avatar
Yuki KODAMA committed
481 482 483 484
                markups = [markups,]
            buf += PANEL_TMPL % (label, markups.pop(0))
            for markup in markups:
                buf += PANEL_TMPL % ('&nbsp;', markup)
Yuki KODAMA's avatar
Yuki KODAMA committed
485
        buf += '</table>'
486
        self.revlabel.setText(buf)
Yuki KODAMA's avatar
Yuki KODAMA committed
487 488 489

        return True

490 491 492 493 494 495 496
    def set_expanded(self, state):
        self.expand_btn.set_expanded(state)
        self.update()

    def is_expanded(self):
        return self.expand_btn.is_expanded()

497 498 499 500
    def minimumSizeHint(self):
        s = QWidget.minimumSizeHint(self)
        return QSize(0, s.height())

Yuki KODAMA's avatar
Yuki KODAMA committed
501 502
LABEL_PAT = re.compile(r'(?:(?<=%%)|(?<!%)%\()(\w+)(?:\)s)')

503
class SummaryLabel(SummaryBase, QLabel):
Yuki KODAMA's avatar
Yuki KODAMA committed
504 505 506

    def __init__(self, target, style, custom, repo, info):
        SummaryBase.__init__(self, target, custom, repo, info)
507 508
        QLabel.__init__(self)

Yuki KODAMA's avatar
Yuki KODAMA committed
509 510 511
        self.csstyle = style

    def update(self, target=None, style=None, custom=None, repo=None):
512
        SummaryBase.update(self, target, custom, repo)
Yuki KODAMA's avatar
Yuki KODAMA committed
513 514 515 516

        if style is not None:
            self.csstyle = style

517 518 519 520 521
        if 'selectable' in self.csstyle:
            sel = self.csstyle['selectable']
            val = sel and Qt.TextSelectableByMouse or Qt.TextBrowserInteraction
            self.setTextInteractionFlags(val)

Yuki KODAMA's avatar
Yuki KODAMA committed
522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542
        if 'width' in self.csstyle:
            width = self.csstyle.get('width', 0)
            self.setMinimumWidth(width)

        if 'height' in self.csstyle:
            height = self.csstyle.get('height', 0)
            self.setMinimumHeight(height)

        contents = self.csstyle.get('contents', None)

        # build info
        info = ''
        for snip in contents:
            # extract all placeholders
            items = LABEL_PAT.findall(snip)
            # fetch required data
            data = {}
            for item in items:
                markups = self.get_markup(item)
                if not markups:
                    continue
543
                if hglib.isbasestring(markups):
Yuki KODAMA's avatar
Yuki KODAMA committed
544 545 546 547 548 549
                    markups = (markups,)
                data[item] = ', '.join(markups)
            if len(data) == 0:
                continue
            # insert data & append to label
            info += snip % data
550
        self.setText(info)
Yuki KODAMA's avatar
Yuki KODAMA committed
551 552

        return True