obshistory.py 26.4 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
    obsutil,
20
    patch,
21
    pycompat,
22
    scmutil,
23
    util,
24
25
26
27
28
)

from mercurial.i18n import _

from . import (
29
    compat,
30
    exthelper,
31
32
)

33
34
eh = exthelper.exthelper()

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

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

47
48
49
50
@eh.extsetup
def addtouchnoise(ui):
    obsutil.METABLACKLIST.append(re.compile(br'^__touch-noise__$'))

51
@eh.command(
52
53
54
55
56
57
58
    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')),
     ] + commands.formatteropts,
59
    _(b'hg olog [OPTION]... [[-r] REV]...'),
60
    **compat.helpcategorykwargs('CATEGORY_CHANGE_NAVIGATION'))
61
def cmdobshistory(ui, repo, *revs, **opts):
62
    """show the obsolescence history of the specified revisions
63

64
65
66
    If no revision range is specified, we display the log for the current
    working copy parent.

67
68
69
70
71
72
73
74
75
76
77
    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.

78
79
    The output is a graph by default but can deactivated with the option
    '--no-graph'.
80
81
82
83
84
85
86
87
88

    '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.
    """
89
    ui.pager(b'obslog')
90
91
    revs = list(revs) + opts['rev']
    if not revs:
92
        revs = [b'.']
93
94
    revs = scmutil.revrange(repo, revs)

95
96
    # Use the default template unless the user provided one.
    if not opts['template']:
97
98
        opts['template'] = DEFAULT_TEMPLATE

99
    if opts['graph']:
100
        return displaygraph(ui, repo, revs, opts)
101
102

    revs.reverse()
103
    displayrevs(ui, repo, revs, opts)
104

105
106
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)}"""
107
108
109
110
TEMPLATE_FIRST_LINE = b"""{if(rev, "%(presentnode)s", "%(missingnode)s")}""" % {
    b"presentnode": TEMPLATE_PRESENT_NODE,
    b"missingnode": TEMPLATE_MISSING_NODE
}
111
TEMPLATE_SUCCNODES = b"""{label("evolve.node", join(succnodes % "{succnode|short}", ", "))}"""
112
TEMPLATE_NODES = b"""{if(succnodes, "as %(succnodes)s")}""" % {
113
114
    b"succnodes": TEMPLATE_SUCCNODES
}
115
TEMPLATE_REWRITE = b"""{label("evolve.verb", verb)}{if(effects, "({join(effects, ", ")})")}"""
116
TEMPLATE_OPERATIONS = b"""{if(operations, "using {label("evolve.operation", join(operations, ", "))}")}"""
117
TEMPLATE_USERS = b"""by {label("evolve.user", join(users, ", "))}"""
118
119
120
121
122
123
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
}
124
TEMPLATE_NOTES = b"""{if(notes, notes % "\n    note: {label("evolve.note", note)}")}"""
125
TEMPLATE_PATCH = b"""{if(patch, "{patch}")}{if(nopatchreason, "\n(No patch available, {nopatchreason})")}"""
126
DEFAULT_TEMPLATE = (b"""%(firstline)s
127
{markers %% "  {separate(" ", "%(rewrite)s", "%(nodes)s", "%(operations)s", "%(users)s", "%(dates)s")}%(notes)s{indent(descdiff, "    ")}{indent("%(patch)s", "    ")}\n"}
128
129
130
""") % {
    b"firstline": TEMPLATE_FIRST_LINE,
    b"rewrite": TEMPLATE_REWRITE,
131
    b"nodes": TEMPLATE_NODES,
132
    b"operations": TEMPLATE_OPERATIONS,
133
    b"users": TEMPLATE_USERS,
134
    b"dates": TEMPLATE_DATES,
135
    b"notes": TEMPLATE_NOTES,
136
    b"patch": TEMPLATE_PATCH,
137
138
}

139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
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)))

163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
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,)))

191
192
193
194
195
196
197
198
199
200
201
202
203
def _nodesandmarkers(repo, ctx, filternonlocal):
    if filternonlocal:
        r = obsutil.successorsandmarkers(repo, ctx)
        if r is None:
            r = []
        for succset in sorted(r):
            if succset[b'markers']:
                yield (succset[b'successors'], succset[b'markers'])
    else:
        markers = repo.obsstore.successors.get(ctx.node(), ())
        for marker in sorted(markers):
            yield (marker[1], [marker])

204
class obsmarker_printer(logcmdutil.changesetprinter):
205
206
207
208
209
    """show (available) information about a node

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

210
211
212
    def __init__(self, ui, repo, *args, **kwargs):

        if kwargs.pop('obspatch', False):
213
            if logcmdutil.changesetdiffer is None:
214
215
216
217
218
219
                kwargs['matchfn'] = scmutil.matchall(repo)
            else:
                kwargs['differ'] = scmutil.matchall(repo)

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

221
        # hg <= 4.6 (3fe1c9263024)
222
        if not util.safehasattr(self, "_includediff"):
223
            self._includediff = diffopts and diffopts.get(b'patch')
224

225
226
        self.template = diffopts and diffopts.get(b'template')
        self.filter = diffopts and diffopts.get(b'filternonlocal')
227

228
229
230
231
232
233
    def show(self, ctx, copies=None, matchfn=None, **props):
        if self.buffered:
            self.ui.pushbuffer(labeled=True)

            changenode = ctx.node()

234
235
            _props = {b"template": self.template}
            fm = self.ui.formatter(b'debugobshistory', _props)
236

237
            displaynode(fm, self.repo, changenode)
238

239
            markerfm = fm.nested(b"markers")
240

241
242
243
244
            data = _nodesandmarkers(self.repo, ctx, self.filter)
            for nodes, markers in data:
                displaymarkers(self.ui, markerfm, nodes, markers, ctx.node(),
                               self.repo, self._includediff)
245

246
247
            markerfm.end()

248
            fm.plain(b'\n')
249
            fm.end()
250

251
252
253
            self.hunk[ctx.node()] = self.ui.popbuffer()
        else:
            ### graph output is buffered only
254
            msg = b'cannot be used outside of the graphlog (yet)'
255
256
257
258
259
260
261
262
            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

263
def patchavailable(node, repo, candidates, successive=True):
264
265
266
267
268
    """ 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
269
    if node not in repo:
270
        return False, b"context is not local"
Boris Feld's avatar
Boris Feld committed
271

272
273
274
275
276
277
278
279
280
281
282
283
    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
284

285
    cand = candidates[0]
Boris Feld's avatar
Boris Feld committed
286

287
288
289
290
291
292
293
294
    if cand not in repo:
        if successive:
            msg = b"successor is unknown locally"
        else:
            msg = b"predecessor is unknown locally"
        return False, msg

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

298
    if nodep1 != candp1 or nodep2 != candp2:
299
        return False, b"changesets rebased"
Boris Feld's avatar
Boris Feld committed
300

301
    return True, cand
Boris Feld's avatar
Boris Feld committed
302

303
def getmarkerdescriptionpatch(repo, basedesc, succdesc):
304
305
    # description are stored without final new line,
    # add one to avoid ugly diff
306
307
    basedesc += b'\n'
    succdesc += b'\n'
308
309

    # fake file name
310
311
    basename = b"changeset-description"
    succname = b"changeset-description"
312

313
314
    uheaders, hunks = mdiff.unidiff(basedesc, b'', succdesc, b'',
                                    basename, succname, False)
315

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

320
    return patch
321

322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
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:
356
        for v in sorted(stack[-1]):
357
358
359
360
361
362
363
364
365
366
367
368
369
370
            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

371
def _obshistorywalker(repo, revs, walksuccessors=False, filternonlocal=False):
372
373
374
375
376
377
    """ 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
378
    candidates, nodesucc, nodeprec = _obshistorywalker_links(repo, revs, walksuccessors)
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393

    # 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
394
        validcandidates = list(filter(isvalidcandidate, candidates))
395

396
397
398
399
        # If we likely have a cycle
        if not validcandidates:
            cycle = cyclic(nodesucc)
            assert cycle
400

401
402
403
            # Then choose a random node from the cycle
            breaknode = sorted(cycle)[0]
            # And display it by force
404
            repo.ui.debug(b'obs-cycle detected, forcing display of %s\n'
405
406
407
408
                          % nodemod.short(breaknode))
            validcandidates = [breaknode]

        # Display all valid candidates
409
410
411
        for cand in sorted(validcandidates):
            # Remove candidate from candidates set
            candidates.remove(cand)
412
413
414
415
416
417
            # And remove it from nodesucc in case of future cycle detected
            try:
                del nodesucc[cand]
            except KeyError:
                pass

418
419
420
421
422
423
            shown.add(cand)

            # Add the right changectx class
            if cand in repo:
                changectx = repo[cand]
            else:
424
425
426
427
                if filternonlocal is False:
                    changectx = missingchangectx(repo, cand)
                else:
                    continue
428

429
430
431
432
            if filternonlocal is False:
                relations = nodeprec.get(cand, ())
            else:
                relations = obsutil.closestpredecessors(repo, cand)
433
434
            parents = [(graphmod.PARENT, x) for x in relations]
            yield (cand, graphmod.CHANGESET, changectx, parents)
435

436
def _obshistorywalker_links(repo, revs, walksuccessors=False):
437
438
    """ Iterate the obs history tree starting from revs, traversing
    each revision precursors recursively.
439
440
441
    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.
442
443
444
445
446
    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
    """
447
    precursors = repo.obsstore.predecessors
448
    successors = repo.obsstore.successors
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
    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)

481
482
483
484
485
486
487
488
        # 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)

489
490
    return sorted(seen), nodesucc, nodeprec

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

493
494
495
    displayer = obsmarker_printer(ui, repo.unfiltered(), obspatch=True,
                                  diffopts=pycompat.byteskwargs(opts),
                                  buffered=True)
496
    edges = graphmod.asciiedges
497
498
    walker = _obshistorywalker(repo.unfiltered(), revs, opts.get('all', False),
                               opts.get('filternonlocal', False))
499
    logcmdutil.displaygraph(ui, repo, walker, displayer, edges)
500

501
def displayrevs(ui, repo, revs, opts):
502
    """ Display the obsolescence history for revset
503
    """
504
    fm = ui.formatter(b'debugobshistory', pycompat.byteskwargs(opts))
505
    predecessors = repo.obsstore.predecessors
506
    successors = repo.obsstore.successors
507
    nodec = repo.changelog.node
508
    unfi = repo.unfiltered()
509
    nodes = [nodec(r) for r in revs]
510
511
    seen = set(nodes)
    toshow = []
512

513
    walksuccessors = opts and opts.get('all')
514
515
    filternonlocal = opts and opts.get('filternonlocal')
    includediff = opts and opts.get('patch')
516
517
518
519

    while nodes:
        ctxnode = nodes.pop()

520
521
522
523
524
        if ctxnode in unfi:
            toshow.append(unfi[ctxnode])
        else:
            if filternonlocal is False:
                toshow.append(missingchangectx(unfi, ctxnode))
525

526
527
        preds = predecessors.get(ctxnode, ())
        for p in sorted(preds):
528
529
530
531
            # Only show nodes once
            if p[0] not in seen:
                seen.add(p[0])
                nodes.append(p[0])
532

533
534
535
536
537
538
539
        if walksuccessors:
            for successor in successors.get(ctxnode, ()):
                for s in successor[1]:
                    if s not in seen:
                        seen.add(s)
                        nodes.append(s)

540
541
542
543
544
    for ctx in toshow:
        displaynode(fm, unfi, ctx.node())

        markerfm = fm.nested(b"markers")

545
        data = _nodesandmarkers(unfi, ctx, filternonlocal)
546
547
548
549
550
551
552
553
        for nodes_, markers in data:
            displaymarkers(ui, markerfm, nodes_, markers, ctx.node(), unfi,
                           includediff)

        markerfm.end()

        fm.plain(b'\n')

554
    fm.end()
555

556
def displaynode(fm, repo, node):
557
    if node in repo:
558
        displayctx(fm, repo[node])
559
    else:
560
        displaymissingctx(fm, node)
561

562
def displayctx(fm, ctx):
563
564
565
    shortdescription = ctx.description().strip()
    if shortdescription:
        shortdescription = shortdescription.splitlines()[0]
566
567

    fm.startitem()
568
    fm.context(ctx=ctx)
569
570
    fm.data(node=ctx.hex())
    fm.plain(b'%s' % bytes(ctx), label=b"evolve.node")
571
    fm.plain(b' ')
572

573
    fm.plain(b'(%d)' % ctx.rev(), label=b"evolve.rev")
574
    fm.plain(b' ')
575

576
577
578
    fm.write(b'shortdescription', b'%s', shortdescription,
             label=b"evolve.short_description")
    fm.plain(b'\n')
579

580
def displaymissingctx(fm, nodewithoutctx):
581
    fm.startitem()
582
583
    fm.data(node=nodemod.hex(nodewithoutctx))
    fm.plain(nodemod.short(nodewithoutctx),
584
585
             label=b"evolve.node evolve.missing_change_ctx")
    fm.plain(b'\n')
586

587
def displaymarkers(ui, fm, succnodes, markers, node, repo, includediff=False):
588
589
    fm.startitem()

590
    verb = _successorsetverb(succnodes, markers)[b"verb"]
591

592
    fm.data(verb=verb)
593

594
    effects = _markerseffects(markers)
595
    if effects:
596
597
        fmteffects = fm.formatlist(effects, b'effect', sep=b', ')
        fm.data(effects=fmteffects)
598

599
    if len(succnodes) > 0:
600
601
        hexnodes = (nodemod.hex(succnode) for succnode in sorted(succnodes))
        nodes = fm.formatlist(hexnodes, b'succnode')
602
        fm.data(succnodes=nodes)
603

604
    # Operations
605
    operations = obsutil.markersoperations(markers)
606
607
    if operations:
        fm.data(operations=fm.formatlist(operations, name=b'operation', sep=b', '))
608

609
    # Users
610
    users = obsutil.markersusers(markers)
611
    fm.data(users=fm.formatlist(users, name=b'user', sep=b', '))
612

613
    # Dates
614
    dates = obsutil.markersdates(markers)
615
    fm.data(dates=fm.formatlist(dates, name=b'date'))
616

617
    # Notes
618
    notes = _markersnotes(markers)
619
620
    if notes:
        fm.data(notes=fm.formatlist(notes, name=b'note', sep=b'\n'))
621

Boris Feld's avatar
Boris Feld committed
622
    # Patch display
623
    if includediff is True:
624
        _patchavailable = patchavailable(node, repo, succnodes)
625
626
627
628
629
630
631
632
633
634
635
636
637

        if _patchavailable[0] is True:
            succ = _patchavailable[1]

            basectx = repo[node]
            succctx = repo[succ]
            # Description patch
            descriptionpatch = getmarkerdescriptionpatch(repo,
                                                         basectx.description(),
                                                         succctx.description())

            if descriptionpatch:
                # add the diffheader
638
                diffheader = b"diff -r %s -r %s changeset-description\n" %\
639
640
641
642
643
644
                             (basectx, succctx)
                descriptionpatch = diffheader + descriptionpatch

                def tolist(text):
                    return [text]

645
646
                ui.pushbuffer(labeled=True)
                ui.write(b"\n")
647
648

                for chunk, label in patch.difflabel(tolist, descriptionpatch):
649
                    chunk = chunk.strip(b'\t')
650
651
                    ui.write(chunk, label=label)
                fm.write(b'descdiff', b'%s', ui.popbuffer())
652
653

            # Content patch
654
            ui.pushbuffer(labeled=True)
655
656
657
            diffopts = patch.diffallopts(repo.ui, {})
            matchfn = scmutil.matchall(repo)
            firstline = True
658
            linestart = True
659
            for chunk, label in patch.diffui(repo, node, succ, matchfn,
660
                                             opts=diffopts):
661
                if firstline:
662
                    ui.write(b'\n')
663
                    firstline = False
664
665
                if linestart:
                    linestart = False
666
                if chunk == b'\n':
667
                    linestart = True
668
                ui.write(chunk, label=label)
669
            fm.data(patch=ui.popbuffer())
670
        else:
671
            fm.data(nopatchreason=_patchavailable[1])
672

Boris Feld's avatar
Boris Feld committed
673
674
675
676
677
678
679
680
681
682
683
684
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"""
685
    try:
686
        lines = next(iterdiff)
687
688
    except StopIteration:
        return None
Boris Feld's avatar
Boris Feld committed
689
    return _prepare_hunk(lines)
690

691
def _getobsfateandsuccs(repo, revnode):
692
    """ Return a tuple containing:
693
    - the reason a revision is obsolete (diverged, pruned or superseded)
694
695
696
    - the list of successors short node if the revision is neither pruned
    or has diverged
    """
697
    successorssets = obsutil.successorssets(repo, revnode)
698
    fate = obsutil._getobsfate(successorssets)
699
700
701
702
703
704

    # 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 = []
705
706
        for succset in successorssets:
            successors.append([nodemod.short(node_id) for node_id in succset])
707
708

    return (fate, successors)
709

710
711
712
713
714
def _markersnotes(markers):
    markersmeta = [dict(m[3]) for m in markers]
    notes = [meta.get(b'note') for meta in markersmeta]
    return sorted(note for note in notes if note)

715
EFFECTMAPPING = util.sortdict([
716
717
718
719
720
721
722
    (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'),
723
724
])

725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
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:
741
742
743
        for key, value in EFFECTMAPPING.items():
            if combined & key:
                effects.append(value)
744
745
746

    return effects

747
VERBMAPPING = {
748
749
750
751
752
753
754
    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"
755
756
}

757
758
759
760
761
762
763
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]
764
765
766
767

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

768
769
770
771
772
773
774
775
776
777
    ef1 = [data.get(b'ef1') for data in metadata]
    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]

778
779
780
def _successorsetverb(successorset, markers):
    """ Return the verb summarizing the successorset
    """
781
    verb = None
782
    if not successorset:
783
        verb = b'pruned'
784
    elif len(successorset) == 1:
785
        verb = _markerspreciseverb(markers)
786
787

        if verb is None:
788
            verb = b'rewritten'
789
    else:
790
791
        verb = b'split'
    return {b'verb': verb}
792

793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
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

813
# Use a more advanced version of obsfateverb that uses effect-flag
814
815
816
@eh.wrapfunction(obsutil, 'obsfateverb')
def obsfateverb(orig, *args, **kwargs):
    return _successorsetverb(*args, **kwargs)[b'verb']