Read about our upcoming Code of Conduct on this issue

obshistory.py 31.3 KB
Newer Older
1
2
3
4
5
6
7
8
9
# Code dedicated to display and exploration of the obsolescence history
#
# This module content aims at being upstreamed enventually.
#
# Copyright 2017 Octobus SAS <contact@octobus.net>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.

10
11
import re

12
from mercurial import (
13
14
    commands,
    error,
15
    graphmod,
16
    logcmdutil,
17
    mdiff,
18
    node as nodemod,
19
20
    obsutil,
    patch,
21
    pycompat,
22
    scmutil,
23
    util,
24
25
)

26
27
from mercurial.utils import dateutil

28
29
30
from mercurial.i18n import _

from . import (
31
    compat,
32
    exthelper,
33
34
)

35
36
eh = exthelper.exthelper()

37
# Config
38
efd = {b'default': True} # pass a default value unless the config is registered
39
40
41
42

@eh.extsetup
def enableeffectflags(ui):
    item = (getattr(ui, '_knownconfig', {})
43
44
            .get(b'experimental', {})
            .get(b'evolution.effect-flags'))
45
46
47
48
    if item is not None:
        item.default = True
        efd.clear()

49
50
51
@eh.extsetup
def addtouchnoise(ui):
    obsutil.METABLACKLIST.append(re.compile(br'^__touch-noise__$'))
52
53
    obsutil.METABLACKLIST.append(re.compile(br'^divergence_source_local$'))
    obsutil.METABLACKLIST.append(re.compile(br'^divergence_source_other$'))
54

55
@eh.command(
56
57
58
59
60
61
    b'obslog|olog',
    [(b'G', b'graph', True, _(b"show the revision DAG")),
     (b'r', b'rev', [], _(b'show the specified revision or revset'), _(b'REV')),
     (b'a', b'all', False, _(b'show all related changesets, not only precursors')),
     (b'p', b'patch', False, _(b'show the patch between two obs versions')),
     (b'f', b'filternonlocal', False, _(b'filter out non local commits')),
62
     (b'o', b'origin', True, _(b'show origin of changesets instead of fate')),
63
     ] + commands.formatteropts,
64
    _(b'hg olog [OPTION]... [[-r] REV]...'),
65
    **compat.helpcategorykwargs('CATEGORY_CHANGE_NAVIGATION'))
66
def cmdobshistory(ui, repo, *revs, **opts):
67
    """show the obsolescence history of the specified revisions
68

69
70
71
    If no revision range is specified, we display the log for the current
    working copy parent.

72
73
74
75
76
77
78
79
80
81
82
    By default this command prints the selected revisions and all its
    precursors. For precursors pointing on existing revisions in the repository,
    it will display revisions node id, revision number and the first line of the
    description. For precursors pointing on non existing revisions in the
    repository (that can happen when exchanging obsolescence-markers), display
    only the node id.

    In both case, for each node, its obsolescence marker will be displayed with
    the obsolescence operation (rewritten or pruned) in addition of the user and
    date of the operation.

83
84
    The output is a graph by default but can deactivated with the option
    '--no-graph'.
85
86
87
88
89
90
91
92
93

    'o' is a changeset, '@' is a working directory parent, 'x' is obsolete,
    and '+' represents a fork where the changeset from the lines below is a
    parent of the 'o' merge on the same line.

    Paths in the DAG are represented with '|', '/' and so forth.

    Returns 0 on success.
    """
94
    ui.pager(b'obslog')
95
96
    revs = list(revs) + opts['rev']
    if not revs:
97
        revs = [b'.']
98
99
    revs = scmutil.revrange(repo, revs)

100
101
    # Use the default template unless the user provided one.
    if not opts['template']:
102
103
        opts['template'] = DEFAULT_TEMPLATE

104
    if opts['graph']:
105
        return displaygraph(ui, repo, revs, opts)
106
107

    revs.reverse()
108
    displayrevs(ui, repo, revs, opts)
109

110
111
TEMPLATE_MISSING_NODE = b"""{label("evolve.node evolve.missing_change_ctx", node|short)}"""
TEMPLATE_PRESENT_NODE = b"""{label("evolve.node", node|short)} {label("evolve.rev", "({rev})")} {label("evolve.short_description", desc|firstline)}"""
112
113
114
115
TEMPLATE_FIRST_LINE = b"""{if(rev, "%(presentnode)s", "%(missingnode)s")}""" % {
    b"presentnode": TEMPLATE_PRESENT_NODE,
    b"missingnode": TEMPLATE_MISSING_NODE
}
116
TEMPLATE_RELNODES = b"""{label("evolve.node", join(relnodes % "{relnode|short}", ", "))}"""
117
TEMPLATE_PREDNODES = b"""{label("evolve.node", join(prednodes % "{prednode|short}", ", "))}"""
118
TEMPLATE_SUCCNODES = b"""{label("evolve.node", join(succnodes % "{succnode|short}", ", "))}"""
119
120
TEMPLATE_NODES = b"""{if(relnodes, "together with %(relnodes)s ")}{if(prednodes, "from %(prednodes)s")}{if(succnodes, "as %(succnodes)s")}""" % {
    b"relnodes": TEMPLATE_RELNODES,
121
    b"prednodes": TEMPLATE_PREDNODES,
122
123
    b"succnodes": TEMPLATE_SUCCNODES
}
124
TEMPLATE_REWRITE = b"""{label("evolve.verb", verb)}{if(effects, "({join(effects, ", ")})")}"""
125
TEMPLATE_OPERATIONS = b"""{if(operations, "using {label("evolve.operation", join(operations, ", "))}")}"""
126
TEMPLATE_USERS = b"""by {label("evolve.user", join(users, ", "))}"""
127
128
129
130
131
132
TEMPLATE_ONE_DATE = b"""({date(max(dates), "%a %b %d %H:%M:%S %Y %1%2")})"""
TEMPLATE_MANY_DATES = b"""(between {date(min(dates), "%a %b %d %H:%M:%S %Y %1%2")} and {date(max(dates), "%a %b %d %H:%M:%S %Y %1%2")})"""
TEMPLATE_DATES = b"""{label("evolve.date", ifeq(min(dates), max(dates), "%(onedate)s", "%(manydates)s"))}""" % {
    b"onedate": TEMPLATE_ONE_DATE,
    b"manydates": TEMPLATE_MANY_DATES
}
133
TEMPLATE_NOTES = b"""{if(notes, notes % "\n    note: {label("evolve.note", note)}")}"""
134
TEMPLATE_PATCH = b"""{if(patch, "{patch}")}{if(nopatchreason, "\n(No patch available, {nopatchreason})")}"""
135
DEFAULT_TEMPLATE = (b"""%(firstline)s
136
{markers %% "  {separate(" ", "%(rewrite)s", "%(nodes)s", "%(operations)s", "%(users)s", "%(dates)s")}%(notes)s{indent(descdiff, "    ")}{indent("%(patch)s", "    ")}\n"}
137
138
139
""") % {
    b"firstline": TEMPLATE_FIRST_LINE,
    b"rewrite": TEMPLATE_REWRITE,
140
    b"nodes": TEMPLATE_NODES,
141
    b"operations": TEMPLATE_OPERATIONS,
142
    b"users": TEMPLATE_USERS,
143
    b"dates": TEMPLATE_DATES,
144
    b"notes": TEMPLATE_NOTES,
145
    b"patch": TEMPLATE_PATCH,
146
147
}

148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
def groupbyfoldid(predsets):
    """ Group nodes and related obsmarkers by fold-id metadata.
    """
    groups = {}
    for (nodes, markers) in predsets:
        grouped = False
        for marker in markers:
            metadata = dict(marker[3])
            foldid = metadata.get(b'fold-id')
            if foldid is not None:
                groups.setdefault(foldid, []).append((nodes, markers))
                grouped = True

        if not grouped:
            yield (nodes, markers)

    for foldid in sorted(groups):
        groupnodes = set()
        groupmarkers = set()
        for (nodes, markers) in groups[foldid]:
            groupnodes.update(nodes)
            groupmarkers.update(markers)
        yield (tuple(sorted(groupnodes)), tuple(sorted(groupmarkers)))

172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
def predecessorsandmarkers(repo, node):
    """ Compute data needed for obsorigin.

    Return a generator of (nodes, markers) tuples, where nodes is a tuple of
    predecessor nodes and markers is a tuple of obsolescence markers.

    Using tuples for everything means no problems with sorted().
    """
    predecessors = repo.obsstore.predecessors
    stack = [(node, ())]
    seen = {node}

    while stack:
        node, path = stack.pop()

        for marker in sorted(predecessors.get(node, ())):
            prednode = marker[0]

            # Basic cycle protection
            if prednode in seen:
                continue
            seen.add(prednode)

            if prednode in repo:
                yield ((prednode,), path + (marker,))
            else:
                stack.append((prednode, path + (marker,)))

200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
def _originmarkers(repo, ctx, filternonlocal):
    predecessors = repo.obsstore.predecessors
    successors = repo.obsstore.successors
    if filternonlocal:
        r = predecessorsandmarkers(repo, ctx.node())
        for (nodes, markers) in sorted(groupbyfoldid(r)):
            yield (nodes, markers)
    else:
        markers = predecessors.get(ctx.node(), ())
        data = (((marker[0],), (marker,)) for marker in markers)
        for (nodes, markers) in sorted(groupbyfoldid(data)):
            yield (nodes, markers)

    # finding prune markers
    for marker in successors.get(ctx.node(), ()):
        if not marker[1]:
            yield ((), (marker,))

218
219
220
221
222
223
def _fatemarkers(repo, ctx, filternonlocal):
    successors = repo.obsstore.successors
    if filternonlocal:
        r = obsutil.successorsandmarkers(repo, ctx)
        if r is None:
            r = []
224
225
226
227
228
229
230
231
        # replacing dicts and sets, they can't be compared
        data = [
            (succset[b'successors'], tuple(sorted(succset[b'markers'])))
            for succset in r
        ]
        for (nodes, markers) in sorted(data):
            if markers:
                yield (nodes, markers)
232
233
234
235
236
    else:
        markers = successors.get(ctx.node(), ())
        for marker in sorted(markers):
            yield (marker[1], [marker])

237
238
239
240
241
242
243
244
245
246
247
248
249
def _nodesandmarkers(repo, ctx, filternonlocal, origin):
    """ Return data for obslog and obsolescence-related template keywords.

    If `filternonlocal` is True, skip filtered nodes (but still gather
    obsolescence markers), otherwise the result will contain nodes unknown
    locally if they are found in the obsolescence markers.

    If `origin` is True, look at predecessors of ctx. Otherwise return
    successors and appropriate obsmarkers.
    """
    if origin:
        for (nodes, markers) in _originmarkers(repo, ctx, filternonlocal):
            yield (nodes, markers)
250
    else:
251
252
        for (nodes, markers) in _fatemarkers(repo, ctx, filternonlocal):
            yield (nodes, markers)
253

254
class obsmarker_printer(logcmdutil.changesetprinter):
255
256
257
258
259
    """show (available) information about a node

    We display the node, description (if available) and various information
    about obsolescence markers affecting it"""

260
261
262
    def __init__(self, ui, repo, *args, **kwargs):

        if kwargs.pop('obspatch', False):
263
            if logcmdutil.changesetdiffer is None:
264
265
266
267
268
269
                kwargs['matchfn'] = scmutil.matchall(repo)
            else:
                kwargs['differ'] = scmutil.matchall(repo)

        super(obsmarker_printer, self).__init__(ui, repo, *args, **kwargs)
        diffopts = kwargs.get('diffopts', {})
270

271
        # hg <= 4.6 (3fe1c9263024)
272
        if not util.safehasattr(self, "_includediff"):
273
            self._includediff = diffopts and diffopts.get(b'patch')
274

275
276
        self.template = diffopts and diffopts.get(b'template')
        self.filter = diffopts and diffopts.get(b'filternonlocal')
277
        self.origin = diffopts and diffopts.get(b'origin')
278

279
280
281
282
283
284
    def show(self, ctx, copies=None, matchfn=None, **props):
        if self.buffered:
            self.ui.pushbuffer(labeled=True)

            changenode = ctx.node()

285
286
            _props = {b"template": self.template}
            fm = self.ui.formatter(b'debugobshistory', _props)
287

288
            displaynode(fm, self.repo, changenode)
289

290
            markerfm = fm.nested(b"markers")
291

292
            data = _nodesandmarkers(self.repo, ctx, self.filter, self.origin)
293
294
            for nodes, markers in data:
                displaymarkers(self.ui, markerfm, nodes, markers, ctx.node(),
295
296
                               self.repo, self._includediff,
                               successive=not self.origin)
297

298
299
            markerfm.end()

300
            fm.plain(b'\n')
301
            fm.end()
302

303
304
305
            self.hunk[ctx.node()] = self.ui.popbuffer()
        else:
            ### graph output is buffered only
306
            msg = b'cannot be used outside of the graphlog (yet)'
307
308
309
310
311
312
313
314
            raise error.ProgrammingError(msg)

    def flush(self, ctx):
        ''' changeset_printer has some logic around buffering data
        in self.headers that we don't use
        '''
        pass

315
def patchavailable(node, repo, candidates, successive=True):
316
317
318
319
320
    """ Check if it's possible to get a diff between node and candidates.

    `candidates` contains nodes, which can be either successors (`successive`
    is True) or predecessors (`successive` is False) of `node`.
    """
Boris Feld's avatar
Boris Feld committed
321
    if node not in repo:
322
        return False, b"context is not local"
Boris Feld's avatar
Boris Feld committed
323

324
325
326
327
328
329
330
331
332
333
334
335
    if len(candidates) == 0:
        if successive:
            msg = b"no successors"
        else:
            msg = b"no predecessors"
        return False, msg
    elif len(candidates) > 1:
        if successive:
            msg = b"too many successors (%d)"
        else:
            msg = b"too many predecessors (%d)"
        return False, msg % len(candidates)
Boris Feld's avatar
Boris Feld committed
336

337
    cand = candidates[0]
Boris Feld's avatar
Boris Feld committed
338

339
340
341
342
343
344
    if cand not in repo:
        if successive:
            msg = b"successor is unknown locally"
        else:
            msg = b"predecessor is unknown locally"
        return False, msg
Boris Feld's avatar
Boris Feld committed
345

346
    # Check that both node and cand have the same parents
Boris Feld's avatar
Boris Feld committed
347
    nodep1, nodep2 = repo[node].p1(), repo[node].p2()
348
    candp1, candp2 = repo[cand].p1(), repo[cand].p2()
Boris Feld's avatar
Boris Feld committed
349

350
    if nodep1 != candp1 or nodep2 != candp2:
351
        return False, b"changesets rebased"
Boris Feld's avatar
Boris Feld committed
352

353
    return True, cand
Boris Feld's avatar
Boris Feld committed
354

355
def getmarkerdescriptionpatch(repo, basedesc, succdesc):
356
357
    # description are stored without final new line,
    # add one to avoid ugly diff
358
359
    basedesc += b'\n'
    succdesc += b'\n'
360
361

    # fake file name
362
363
    basename = b"changeset-description"
    succname = b"changeset-description"
364

365
366
    uheaders, hunks = mdiff.unidiff(basedesc, b'', succdesc, b'',
                                    basename, succname, False)
367
368

    # Copied from patch.diff
369
370
    text = b''.join(sum((list(hlines) for hrange, hlines in hunks), []))
    patch = b"\n".join(uheaders + [text])
371

372
    return patch
373

374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
class missingchangectx(object):
    ''' a minimal object mimicking changectx for change contexts
    references by obs markers but not available locally '''

    def __init__(self, repo, nodeid):
        self._repo = repo
        self._node = nodeid

    def node(self):
        return self._node

    def obsolete(self):
        # If we don't have it locally, it's obsolete
        return True

def cyclic(graph):
    """Return True if the directed graph has a cycle.
    The graph must be represented as a dictionary mapping vertices to
    iterables of neighbouring vertices. For example:

    >>> cyclic({1: (2,), 2: (3,), 3: (1,)})
    True
    >>> cyclic({1: (2,), 2: (3,), 3: (4,)})
    False

    Taken from: https://codereview.stackexchange.com/a/86067

    """
    visited = set()
    o = object()
    path = [o]
    path_set = set(path)
    stack = [iter(graph)]
    while stack:
408
        for v in sorted(stack[-1]):
409
410
411
412
413
414
415
416
417
418
419
420
421
422
            if v in path_set:
                path_set.remove(o)
                return path_set
            elif v not in visited:
                visited.add(v)
                path.append(v)
                path_set.add(v)
                stack.append(iter(graph.get(v, ())))
                break
        else:
            path_set.remove(path.pop())
            stack.pop()
    return False

423
def _obshistorywalker(repo, revs, walksuccessors=False, filternonlocal=False):
424
425
426
427
428
429
    """ Directly inspired by graphmod.dagwalker,
    walk the obs marker tree and yield
    (id, CHANGESET, ctx, [parentinfo]) tuples
    """

    # Get the list of nodes and links between them
430
    candidates, nodesucc, nodeprec = _obshistorywalker_links(repo, revs, walksuccessors)
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445

    # Shown, set of nodes presents in items
    shown = set()

    def isvalidcandidate(candidate):
        """ Function to filter candidates, check the candidate succ are
        in shown set
        """
        return nodesucc.get(candidate, set()).issubset(shown)

    # While we have some nodes to show
    while candidates:

        # Filter out candidates, returns only nodes with all their successors
        # already shown
446
        validcandidates = list(filter(isvalidcandidate, candidates))
447

448
449
450
451
452
453
454
455
        # If we likely have a cycle
        if not validcandidates:
            cycle = cyclic(nodesucc)
            assert cycle

            # Then choose a random node from the cycle
            breaknode = sorted(cycle)[0]
            # And display it by force
456
            repo.ui.debug(b'obs-cycle detected, forcing display of %s\n'
457
458
                          % nodemod.short(breaknode))
            validcandidates = [breaknode]
459

460
        # Display all valid candidates
461
462
463
        for cand in sorted(validcandidates):
            # Remove candidate from candidates set
            candidates.remove(cand)
464
465
466
467
468
469
            # And remove it from nodesucc in case of future cycle detected
            try:
                del nodesucc[cand]
            except KeyError:
                pass

470
471
472
473
474
475
            shown.add(cand)

            # Add the right changectx class
            if cand in repo:
                changectx = repo[cand]
            else:
476
477
478
479
                if filternonlocal is False:
                    changectx = missingchangectx(repo, cand)
                else:
                    continue
480

481
482
483
484
            if filternonlocal is False:
                relations = nodeprec.get(cand, ())
            else:
                relations = obsutil.closestpredecessors(repo, cand)
485
486
            parents = [(graphmod.PARENT, x) for x in relations]
            yield (cand, graphmod.CHANGESET, changectx, parents)
487

488
def _obshistorywalker_links(repo, revs, walksuccessors=False):
489
490
    """ Iterate the obs history tree starting from revs, traversing
    each revision precursors recursively.
491
492
493
    If walksuccessors is True, also check that every successor has been
    walked, which ends up walking on all connected obs markers. It helps
    getting a better view with splits and divergences.
494
495
496
497
498
    Return a tuple of:
    - The list of node crossed
    - The dictionnary of each node successors, values are a set
    - The dictionnary of each node precursors, values are a list
    """
499
    precursors = repo.obsstore.predecessors
500
    successors = repo.obsstore.successors
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
    nodec = repo.changelog.node

    # Parents, set of parents nodes seen during walking the graph for node
    nodesucc = dict()
    # Childrens
    nodeprec = dict()

    nodes = [nodec(r) for r in revs]
    seen = set(nodes)

    # Iterate on each node
    while nodes:
        node = nodes.pop()

        precs = precursors.get(node, ())

        nodeprec[node] = []

        for prec in sorted(precs):
            precnode = prec[0]

            # Mark node as prec successor
            nodesucc.setdefault(precnode, set()).add(node)

            # Mark precnode as node precursor
            nodeprec[node].append(precnode)

            # Add prec for future processing if not node already processed
            if precnode not in seen:
                seen.add(precnode)
                nodes.append(precnode)

533
534
535
536
537
538
539
540
        # Also walk on successors if the option is enabled
        if walksuccessors:
            for successor in successors.get(node, ()):
                for succnodeid in successor[1]:
                    if succnodeid not in seen:
                        seen.add(succnodeid)
                        nodes.append(succnodeid)

541
542
    return sorted(seen), nodesucc, nodeprec

543
def displaygraph(ui, repo, revs, opts):
Boris Feld's avatar
Boris Feld committed
544

545
546
547
    displayer = obsmarker_printer(ui, repo.unfiltered(), obspatch=True,
                                  diffopts=pycompat.byteskwargs(opts),
                                  buffered=True)
548
    edges = graphmod.asciiedges
549
550
    walker = _obshistorywalker(repo.unfiltered(), revs, opts.get('all', False),
                               opts.get('filternonlocal', False))
551
    logcmdutil.displaygraph(ui, repo, walker, displayer, edges)
552

553
def displayrevs(ui, repo, revs, opts):
554
    """ Display the obsolescence history for revset
555
    """
556
    fm = ui.formatter(b'debugobshistory', pycompat.byteskwargs(opts))
557
    predecessors = repo.obsstore.predecessors
558
    successors = repo.obsstore.successors
559
    nodec = repo.changelog.node
560
    unfi = repo.unfiltered()
561
562
    nodes = [nodec(r) for r in revs]
    seen = set(nodes)
563
564
    toshow = []

565
    origin = opts and opts.get('origin')
566
    walksuccessors = opts and opts.get('all')
567
568
    filternonlocal = opts and opts.get('filternonlocal')
    includediff = opts and opts.get('patch')
569
570
571
572

    while nodes:
        ctxnode = nodes.pop()

573
574
575
576
577
        if ctxnode in unfi:
            toshow.append(unfi[ctxnode])
        else:
            if filternonlocal is False:
                toshow.append(missingchangectx(unfi, ctxnode))
578

579
580
        preds = predecessors.get(ctxnode, ())
        for p in sorted(preds):
581
582
583
584
            # Only show nodes once
            if p[0] not in seen:
                seen.add(p[0])
                nodes.append(p[0])
585

586
587
588
589
590
591
592
        if walksuccessors:
            for successor in successors.get(ctxnode, ()):
                for s in successor[1]:
                    if s not in seen:
                        seen.add(s)
                        nodes.append(s)

593
594
595
596
597
    for ctx in toshow:
        displaynode(fm, unfi, ctx.node())

        markerfm = fm.nested(b"markers")

598
        data = _nodesandmarkers(unfi, ctx, filternonlocal, origin)
599
600
        for nodes_, markers in data:
            displaymarkers(ui, markerfm, nodes_, markers, ctx.node(), unfi,
601
                           includediff, successive=not origin)
602
603
604
605
606

        markerfm.end()

        fm.plain(b'\n')

607
    fm.end()
608

609
def displaynode(fm, repo, node):
610
    if node in repo:
611
        displayctx(fm, repo[node])
612
    else:
613
        displaymissingctx(fm, node)
614

615
def displayctx(fm, ctx):
616
617
618
    shortdescription = ctx.description().strip()
    if shortdescription:
        shortdescription = shortdescription.splitlines()[0]
619
620

    fm.startitem()
621
    fm.context(ctx=ctx)
622
623
    fm.data(node=ctx.hex())
    fm.plain(b'%s' % bytes(ctx), label=b"evolve.node")
624
    fm.plain(b' ')
625

626
    fm.plain(b'(%d)' % ctx.rev(), label=b"evolve.rev")
627
    fm.plain(b' ')
628

629
630
631
    fm.write(b'shortdescription', b'%s', shortdescription,
             label=b"evolve.short_description")
    fm.plain(b'\n')
632

633
def displaymissingctx(fm, nodewithoutctx):
634
    fm.startitem()
635
636
    fm.data(node=nodemod.hex(nodewithoutctx))
    fm.plain(nodemod.short(nodewithoutctx),
637
638
             label=b"evolve.node evolve.missing_change_ctx")
    fm.plain(b'\n')
639

640
641
def displaymarkers(ui, fm, nodes, markers, node, repo, includediff=False,
                   successive=True):
642
643
    fm.startitem()

644
645
646
647
    if successive:
        verb = _successorsetverb(nodes, markers)[b"verb"]
    else:
        verb = _predecessorsverb(nodes, markers)
648

649
    fm.data(verb=verb)
650

651
    effects = _markerseffects(markers)
652
    if effects:
653
654
        fmteffects = fm.formatlist(effects, b'effect', sep=b', ')
        fm.data(effects=fmteffects)
655

656
657
658
659
660
661
662
663
    if len(nodes) > 0:
        hexnodes = (nodemod.hex(node) for node in sorted(nodes))
        if successive:
            nodelist = fm.formatlist(hexnodes, b'succnode')
            fm.data(succnodes=nodelist)
        else:
            nodelist = fm.formatlist(hexnodes, b'prednode')
            fm.data(prednodes=nodelist)
664

665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
        relnodes = set()
        if successive:
            # TODO: show other parts of folds
            pass
        else:
            for marker in markers:
                if len(marker[1]) > 1:
                    relnodes |= set(marker[1])
        relnodes.discard(node)

        if relnodes:
            relnodes = (nodemod.hex(node) for node in sorted(relnodes))
            nodelist = fm.formatlist(relnodes, b'relnode')
            fm.data(relnodes=nodelist)

680
    # Operations
681
    operations = obsutil.markersoperations(markers)
682
683
    if operations:
        fm.data(operations=fm.formatlist(operations, name=b'operation', sep=b', '))
684

685
    # Users
686
    users = obsutil.markersusers(markers)
687
    fm.data(users=fm.formatlist(users, name=b'user', sep=b', '))
688

689
    # Dates
690
    dates = obsutil.markersdates(markers)
691
    fm.data(dates=fm.formatlist(dates, name=b'date'))
692

693
    # Notes
694
    notes = _markersnotes(markers)
695
696
    if notes:
        fm.data(notes=fm.formatlist(notes, name=b'note', sep=b'\n'))
697

Boris Feld's avatar
Boris Feld committed
698
    # Patch display
699
    if includediff is True:
700
701
        _patchavailable = patchavailable(node, repo, nodes,
                                         successive=successive)
702
703

        if _patchavailable[0] is True:
704
            diffnode = _patchavailable[1]
705

706
707
708
709
710
711
            if successive:
                actx = repo[node]
                bctx = repo[diffnode]
            else:
                actx = repo[diffnode]
                bctx = repo[node]
712
713
            # Description patch
            descriptionpatch = getmarkerdescriptionpatch(repo,
714
715
                                                         actx.description(),
                                                         bctx.description())
716
717
718

            if descriptionpatch:
                # add the diffheader
719
                diffheader = b"diff -r %s -r %s changeset-description\n" %\
720
                             (actx, bctx)
721
722
723
724
725
                descriptionpatch = diffheader + descriptionpatch

                def tolist(text):
                    return [text]

726
727
                ui.pushbuffer(labeled=True)
                ui.write(b"\n")
728
729

                for chunk, label in patch.difflabel(tolist, descriptionpatch):
730
                    chunk = chunk.strip(b'\t')
731
732
                    ui.write(chunk, label=label)
                fm.write(b'descdiff', b'%s', ui.popbuffer())
733
734

            # Content patch
735
            ui.pushbuffer(labeled=True)
736
737
738
            diffopts = patch.diffallopts(repo.ui, {})
            matchfn = scmutil.matchall(repo)
            firstline = True
739
            linestart = True
740
741
            for chunk, label in patch.diffui(repo, actx.node(), bctx.node(),
                                             matchfn, opts=diffopts):
742
                if firstline:
743
                    ui.write(b'\n')
744
                    firstline = False
745
746
                if linestart:
                    linestart = False
747
                if chunk == b'\n':
748
                    linestart = True
749
                ui.write(chunk, label=label)
750
            fm.data(patch=ui.popbuffer())
751
        else:
752
            fm.data(nopatchreason=_patchavailable[1])
753

Boris Feld's avatar
Boris Feld committed
754
755
756
757
758
759
760
761
762
763
764
765
def _prepare_hunk(hunk):
    """Drop all information but the username and patch"""
    cleanunk = []
    for line in hunk.splitlines():
        if line.startswith(b'# User') or not line.startswith(b'#'):
            if line.startswith(b'@@'):
                line = b'@@\n'
            cleanunk.append(line)
    return cleanunk

def _getdifflines(iterdiff):
    """return a cleaned up lines"""
766
    try:
767
        lines = next(iterdiff)
768
769
    except StopIteration:
        return None
Boris Feld's avatar
Boris Feld committed
770
    return _prepare_hunk(lines)
771

772
def _getobsfateandsuccs(repo, revnode):
773
    """ Return a tuple containing:
774
    - the reason a revision is obsolete (diverged, pruned or superseded)
775
776
777
    - the list of successors short node if the revision is neither pruned
    or has diverged
    """
778
    successorssets = obsutil.successorssets(repo, revnode)
779
    fate = obsutil._getobsfate(successorssets)
780
781
782
783
784
785

    # Apply node.short if we have no divergence
    if len(successorssets) == 1:
        successors = [nodemod.short(node_id) for node_id in successorssets[0]]
    else:
        successors = []
786
787
        for succset in successorssets:
            successors.append([nodemod.short(node_id) for node_id in succset])
788
789

    return (fate, successors)
790

791
792
def _markersnotes(markers):
    markersmeta = [dict(m[3]) for m in markers]
793
    notes = {meta.get(b'note') for meta in markersmeta}
794
795
    return sorted(note for note in notes if note)

796
EFFECTMAPPING = util.sortdict([
797
798
799
800
801
802
803
    (obsutil.DESCCHANGED, b'description'),
    (obsutil.METACHANGED, b'meta'),
    (obsutil.USERCHANGED, b'user'),
    (obsutil.DATECHANGED, b'date'),
    (obsutil.BRANCHCHANGED, b'branch'),
    (obsutil.PARENTCHANGED, b'parent'),
    (obsutil.DIFFCHANGED, b'content'),
804
805
])

806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
def _markerseffects(markers):
    """ Return a list of effects as strings based on effect flags in markers

    Return None if verb cannot be more precise than just "rewritten", i.e. when
    markers collectively have more than one effect in the flags.
    """
    metadata = [dict(marker[3]) for marker in markers]
    ef1 = [data.get(b'ef1') for data in metadata]
    effects = []

    combined = 0
    for ef in ef1:
        if ef:
            combined |= int(ef)

    if combined:
822
823
824
        for key, value in EFFECTMAPPING.items():
            if combined & key:
                effects.append(value)
825
826
827

    return effects

828
VERBMAPPING = {
829
830
831
832
833
834
835
    obsutil.DESCCHANGED: b"reworded",
    obsutil.METACHANGED: b"meta-changed",
    obsutil.USERCHANGED: b"reauthored",
    obsutil.DATECHANGED: b"date-changed",
    obsutil.BRANCHCHANGED: b"branch-changed",
    obsutil.PARENTCHANGED: b"rebased",
    obsutil.DIFFCHANGED: b"amended"
836
837
}

838
839
840
841
842
843
844
845
def _markerspreciseverb(markers):
    """ Return a more precise verb based on effect flags in markers

    Return None if verb cannot be more precise than just "rewritten", i.e. when
    markers collectively have more than one effect in the flags.
    """
    metadata = [dict(marker[3]) for marker in markers]

846
847
848
849
    if len(metadata) == 1 and metadata[0].get(b'fold-id') is not None:
        return b'folded'

    ef1 = [data.get(b'ef1') for data in metadata]
850
851
852
853
854
855
856
857
858
    if all(ef1):
        combined = 0
        for ef in ef1:
            combined |= int(ef)

        # Combined will be in VERBMAPPING only if one bit is set
        if combined in VERBMAPPING:
            return VERBMAPPING[combined]

859
860
861
def _successorsetverb(successorset, markers):
    """ Return the verb summarizing the successorset
    """
862
    verb = None
863
    if not successorset:
864
        verb = b'pruned'
865
    elif len(successorset) == 1:
866
        verb = _markerspreciseverb(markers)
867
868

        if verb is None:
869
            verb = b'rewritten'
870
    else:
871
872
        verb = b'split'
    return {b'verb': verb}
873

874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
def _predecessorsverb(predecessors, markers):
    """ Return the verb summarizing a set of predecessors and related markers.
    """
    verb = None
    if not predecessors:
        # we got successors instead of predecessors, and they are empty
        # (this is a special case for showing prunes)
        verb = b'pruned'
    elif len(markers) == 1 and len(markers[0][1]) > 1:
        # peeked at the successors to see if this is a split
        verb = b'split'
    elif len(predecessors) == 1:
        verb = _markerspreciseverb(markers)

        if verb is None:
            verb = b'rewritten'
    else:
        verb = b'folded'
    return verb

894
# Use a more advanced version of obsfateverb that uses effect-flag
895
896
897
@eh.wrapfunction(obsutil, 'obsfateverb')
def obsfateverb(orig, *args, **kwargs):
    return _successorsetverb(*args, **kwargs)[b'verb']
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955

def obsoriginprinter(ui, repo, predecessors, markers):
    """ Build an obsorigin string for a single set of predecessors.
    """
    quiet = ui.quiet
    verbose = ui.verbose
    normal = not verbose and not quiet

    line = []

    # Verb
    line.append(_predecessorsverb(predecessors, markers))

    # Operations
    operations = obsutil.markersoperations(markers)
    if operations:
        line.append(b" using %s" % b", ".join(operations))

    # Predecessors
    if predecessors:
        unfi = repo.unfiltered()

        def formatnode(node):
            if node in unfi:
                return scmutil.formatchangeid(unfi[node])
            return nodemod.short(node)

        fmtpredecessors = [formatnode(pred) for pred in predecessors]
        line.append(b" from %s" % b", ".join(sorted(fmtpredecessors)))

    # Users
    users = obsutil.markersusers(markers)
    # Filter out current user in not verbose mode to reduce amount of
    # information
    if not verbose:
        currentuser = ui.username(acceptempty=True)
        if len(users) == 1 and currentuser in users:
            users = None

    if (verbose or normal) and users:
        line.append(b" by %s" % b", ".join(users))

    # Dates
    dates = obsutil.markersdates(markers)

    if dates and verbose:
        min_date = min(dates)
        max_date = max(dates)

        if min_date == max_date:
            fmtmin_date = dateutil.datestr(min_date, b'%Y-%m-%d %H:%M %1%2')
            line.append(b" (at %s)" % fmtmin_date)
        else:
            fmtmin_date = dateutil.datestr(min_date, b'%Y-%m-%d %H:%M %1%2')
            fmtmax_date = dateutil.datestr(max_date, b'%Y-%m-%d %H:%M %1%2')
            line.append(b" (between %s and %s)" % (fmtmin_date, fmtmax_date))

    return b"".join(line)