1# mq.py - patch queues for mercurial
2#
3# Copyright 2005, 2006 Chris Mason <mason@suse.com>
4#
5# This software may be used and distributed according to the terms of the
6# GNU General Public License version 2 or any later version.
7
8'''manage a stack of patches
9
10This extension lets you work with a stack of patches in a Mercurial
11repository. It manages two stacks of patches - all known patches, and
12applied patches (subset of known patches).
13
14Known patches are represented as patch files in the .hg/patches
15directory. Applied patches are both patch files and changesets.
16
17Common tasks (use :hg:`help COMMAND` for more details)::
18
19  create new patch                          qnew
20  import existing patch                     qimport
21
22  print patch series                        qseries
23  print applied patches                     qapplied
24
25  add known patch to applied stack          qpush
26  remove patch from applied stack           qpop
27  refresh contents of top applied patch     qrefresh
28
29By default, mq will automatically use git patches when required to
30avoid losing file mode changes, copy records, binary files or empty
31files creations or deletions. This behavior can be configured with::
32
33  [mq]
34  git = auto/keep/yes/no
35
36If set to 'keep', mq will obey the [diff] section configuration while
37preserving existing git patches upon qrefresh. If set to 'yes' or
38'no', mq will override the [diff] section and always generate git or
39regular patches, possibly losing data in the second case.
40
41It may be desirable for mq changesets to be kept in the secret phase (see
42:hg:`help phases`), which can be enabled with the following setting::
43
44  [mq]
45  secret = True
46
47You will by default be managing a patch queue named "patches". You can
48create other, independent patch queues with the :hg:`qqueue` command.
49
50If the working directory contains uncommitted files, qpush, qpop and
51qgoto abort immediately. If -f/--force is used, the changes are
52discarded. Setting::
53
54  [mq]
55  keepchanges = True
56
57make them behave as if --keep-changes were passed, and non-conflicting
58local changes will be tolerated and preserved. If incompatible options
59such as -f/--force or --exact are passed, this setting is ignored.
60
61This extension used to provide a strip command. This command now lives
62in the strip extension.
63'''
64
65from __future__ import absolute_import, print_function
66
67import errno
68import os
69import re
70import shutil
71import sys
72from mercurial.i18n import _
73from mercurial.node import (
74    bin,
75    hex,
76    nullrev,
77    short,
78)
79from mercurial.pycompat import (
80    delattr,
81    getattr,
82    open,
83)
84from mercurial import (
85    cmdutil,
86    commands,
87    dirstateguard,
88    encoding,
89    error,
90    extensions,
91    hg,
92    localrepo,
93    lock as lockmod,
94    logcmdutil,
95    patch as patchmod,
96    phases,
97    pycompat,
98    registrar,
99    revsetlang,
100    scmutil,
101    smartset,
102    strip,
103    subrepoutil,
104    util,
105    vfs as vfsmod,
106)
107from mercurial.utils import (
108    dateutil,
109    stringutil,
110    urlutil,
111)
112
113release = lockmod.release
114seriesopts = [(b's', b'summary', None, _(b'print first line of patch header'))]
115
116cmdtable = {}
117command = registrar.command(cmdtable)
118# Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
119# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
120# be specifying the version(s) of Mercurial they are tested with, or
121# leave the attribute unspecified.
122testedwith = b'ships-with-hg-core'
123
124configtable = {}
125configitem = registrar.configitem(configtable)
126
127configitem(
128    b'mq',
129    b'git',
130    default=b'auto',
131)
132configitem(
133    b'mq',
134    b'keepchanges',
135    default=False,
136)
137configitem(
138    b'mq',
139    b'plain',
140    default=False,
141)
142configitem(
143    b'mq',
144    b'secret',
145    default=False,
146)
147
148# force load strip extension formerly included in mq and import some utility
149try:
150    extensions.find(b'strip')
151except KeyError:
152    # note: load is lazy so we could avoid the try-except,
153    # but I (marmoute) prefer this explicit code.
154    class dummyui(object):
155        def debug(self, msg):
156            pass
157
158        def log(self, event, msgfmt, *msgargs, **opts):
159            pass
160
161    extensions.load(dummyui(), b'strip', b'')
162
163strip = strip.strip
164
165
166def checksubstate(repo, baserev=None):
167    """return list of subrepos at a different revision than substate.
168    Abort if any subrepos have uncommitted changes."""
169    inclsubs = []
170    wctx = repo[None]
171    if baserev:
172        bctx = repo[baserev]
173    else:
174        bctx = wctx.p1()
175    for s in sorted(wctx.substate):
176        wctx.sub(s).bailifchanged(True)
177        if s not in bctx.substate or bctx.sub(s).dirty():
178            inclsubs.append(s)
179    return inclsubs
180
181
182# Patch names looks like unix-file names.
183# They must be joinable with queue directory and result in the patch path.
184normname = util.normpath
185
186
187class statusentry(object):
188    def __init__(self, node, name):
189        self.node, self.name = node, name
190
191    def __bytes__(self):
192        return hex(self.node) + b':' + self.name
193
194    __str__ = encoding.strmethod(__bytes__)
195    __repr__ = encoding.strmethod(__bytes__)
196
197
198# The order of the headers in 'hg export' HG patches:
199HGHEADERS = [
200    #   '# HG changeset patch',
201    b'# User ',
202    b'# Date ',
203    b'#      ',
204    b'# Branch ',
205    b'# Node ID ',
206    b'# Parent  ',  # can occur twice for merges - but that is not relevant for mq
207]
208# The order of headers in plain 'mail style' patches:
209PLAINHEADERS = {
210    b'from': 0,
211    b'date': 1,
212    b'subject': 2,
213}
214
215
216def inserthgheader(lines, header, value):
217    """Assuming lines contains a HG patch header, add a header line with value.
218    >>> try: inserthgheader([], b'# Date ', b'z')
219    ... except ValueError as inst: print("oops")
220    oops
221    >>> inserthgheader([b'# HG changeset patch'], b'# Date ', b'z')
222    ['# HG changeset patch', '# Date z']
223    >>> inserthgheader([b'# HG changeset patch', b''], b'# Date ', b'z')
224    ['# HG changeset patch', '# Date z', '']
225    >>> inserthgheader([b'# HG changeset patch', b'# User y'], b'# Date ', b'z')
226    ['# HG changeset patch', '# User y', '# Date z']
227    >>> inserthgheader([b'# HG changeset patch', b'# Date x', b'# User y'],
228    ...                b'# User ', b'z')
229    ['# HG changeset patch', '# Date x', '# User z']
230    >>> inserthgheader([b'# HG changeset patch', b'# Date y'], b'# Date ', b'z')
231    ['# HG changeset patch', '# Date z']
232    >>> inserthgheader([b'# HG changeset patch', b'', b'# Date y'],
233    ...                b'# Date ', b'z')
234    ['# HG changeset patch', '# Date z', '', '# Date y']
235    >>> inserthgheader([b'# HG changeset patch', b'# Parent  y'],
236    ...                b'# Date ', b'z')
237    ['# HG changeset patch', '# Date z', '# Parent  y']
238    """
239    start = lines.index(b'# HG changeset patch') + 1
240    newindex = HGHEADERS.index(header)
241    bestpos = len(lines)
242    for i in range(start, len(lines)):
243        line = lines[i]
244        if not line.startswith(b'# '):
245            bestpos = min(bestpos, i)
246            break
247        for lineindex, h in enumerate(HGHEADERS):
248            if line.startswith(h):
249                if lineindex == newindex:
250                    lines[i] = header + value
251                    return lines
252                if lineindex > newindex:
253                    bestpos = min(bestpos, i)
254                break  # next line
255    lines.insert(bestpos, header + value)
256    return lines
257
258
259def insertplainheader(lines, header, value):
260    """For lines containing a plain patch header, add a header line with value.
261    >>> insertplainheader([], b'Date', b'z')
262    ['Date: z']
263    >>> insertplainheader([b''], b'Date', b'z')
264    ['Date: z', '']
265    >>> insertplainheader([b'x'], b'Date', b'z')
266    ['Date: z', '', 'x']
267    >>> insertplainheader([b'From: y', b'x'], b'Date', b'z')
268    ['From: y', 'Date: z', '', 'x']
269    >>> insertplainheader([b' date : x', b' from : y', b''], b'From', b'z')
270    [' date : x', 'From: z', '']
271    >>> insertplainheader([b'', b'Date: y'], b'Date', b'z')
272    ['Date: z', '', 'Date: y']
273    >>> insertplainheader([b'foo: bar', b'DATE: z', b'x'], b'From', b'y')
274    ['From: y', 'foo: bar', 'DATE: z', '', 'x']
275    """
276    newprio = PLAINHEADERS[header.lower()]
277    bestpos = len(lines)
278    for i, line in enumerate(lines):
279        if b':' in line:
280            lheader = line.split(b':', 1)[0].strip().lower()
281            lprio = PLAINHEADERS.get(lheader, newprio + 1)
282            if lprio == newprio:
283                lines[i] = b'%s: %s' % (header, value)
284                return lines
285            if lprio > newprio and i < bestpos:
286                bestpos = i
287        else:
288            if line:
289                lines.insert(i, b'')
290            if i < bestpos:
291                bestpos = i
292            break
293    lines.insert(bestpos, b'%s: %s' % (header, value))
294    return lines
295
296
297class patchheader(object):
298    def __init__(self, pf, plainmode=False):
299        def eatdiff(lines):
300            while lines:
301                l = lines[-1]
302                if (
303                    l.startswith(b"diff -")
304                    or l.startswith(b"Index:")
305                    or l.startswith(b"===========")
306                ):
307                    del lines[-1]
308                else:
309                    break
310
311        def eatempty(lines):
312            while lines:
313                if not lines[-1].strip():
314                    del lines[-1]
315                else:
316                    break
317
318        message = []
319        comments = []
320        user = None
321        date = None
322        parent = None
323        format = None
324        subject = None
325        branch = None
326        nodeid = None
327        diffstart = 0
328
329        for line in open(pf, b'rb'):
330            line = line.rstrip()
331            if line.startswith(b'diff --git') or (
332                diffstart and line.startswith(b'+++ ')
333            ):
334                diffstart = 2
335                break
336            diffstart = 0  # reset
337            if line.startswith(b"--- "):
338                diffstart = 1
339                continue
340            elif format == b"hgpatch":
341                # parse values when importing the result of an hg export
342                if line.startswith(b"# User "):
343                    user = line[7:]
344                elif line.startswith(b"# Date "):
345                    date = line[7:]
346                elif line.startswith(b"# Parent "):
347                    parent = line[9:].lstrip()  # handle double trailing space
348                elif line.startswith(b"# Branch "):
349                    branch = line[9:]
350                elif line.startswith(b"# Node ID "):
351                    nodeid = line[10:]
352                elif not line.startswith(b"# ") and line:
353                    message.append(line)
354                    format = None
355            elif line == b'# HG changeset patch':
356                message = []
357                format = b"hgpatch"
358            elif format != b"tagdone" and (
359                line.startswith(b"Subject: ") or line.startswith(b"subject: ")
360            ):
361                subject = line[9:]
362                format = b"tag"
363            elif format != b"tagdone" and (
364                line.startswith(b"From: ") or line.startswith(b"from: ")
365            ):
366                user = line[6:]
367                format = b"tag"
368            elif format != b"tagdone" and (
369                line.startswith(b"Date: ") or line.startswith(b"date: ")
370            ):
371                date = line[6:]
372                format = b"tag"
373            elif format == b"tag" and line == b"":
374                # when looking for tags (subject: from: etc) they
375                # end once you find a blank line in the source
376                format = b"tagdone"
377            elif message or line:
378                message.append(line)
379            comments.append(line)
380
381        eatdiff(message)
382        eatdiff(comments)
383        # Remember the exact starting line of the patch diffs before consuming
384        # empty lines, for external use by TortoiseHg and others
385        self.diffstartline = len(comments)
386        eatempty(message)
387        eatempty(comments)
388
389        # make sure message isn't empty
390        if format and format.startswith(b"tag") and subject:
391            message.insert(0, subject)
392
393        self.message = message
394        self.comments = comments
395        self.user = user
396        self.date = date
397        self.parent = parent
398        # nodeid and branch are for external use by TortoiseHg and others
399        self.nodeid = nodeid
400        self.branch = branch
401        self.haspatch = diffstart > 1
402        self.plainmode = (
403            plainmode
404            or b'# HG changeset patch' not in self.comments
405            and any(
406                c.startswith(b'Date: ') or c.startswith(b'From: ')
407                for c in self.comments
408            )
409        )
410
411    def setuser(self, user):
412        try:
413            inserthgheader(self.comments, b'# User ', user)
414        except ValueError:
415            if self.plainmode:
416                insertplainheader(self.comments, b'From', user)
417            else:
418                tmp = [b'# HG changeset patch', b'# User ' + user]
419                self.comments = tmp + self.comments
420        self.user = user
421
422    def setdate(self, date):
423        try:
424            inserthgheader(self.comments, b'# Date ', date)
425        except ValueError:
426            if self.plainmode:
427                insertplainheader(self.comments, b'Date', date)
428            else:
429                tmp = [b'# HG changeset patch', b'# Date ' + date]
430                self.comments = tmp + self.comments
431        self.date = date
432
433    def setparent(self, parent):
434        try:
435            inserthgheader(self.comments, b'# Parent  ', parent)
436        except ValueError:
437            if not self.plainmode:
438                tmp = [b'# HG changeset patch', b'# Parent  ' + parent]
439                self.comments = tmp + self.comments
440        self.parent = parent
441
442    def setmessage(self, message):
443        if self.comments:
444            self._delmsg()
445        self.message = [message]
446        if message:
447            if self.plainmode and self.comments and self.comments[-1]:
448                self.comments.append(b'')
449            self.comments.append(message)
450
451    def __bytes__(self):
452        s = b'\n'.join(self.comments).rstrip()
453        if not s:
454            return b''
455        return s + b'\n\n'
456
457    __str__ = encoding.strmethod(__bytes__)
458
459    def _delmsg(self):
460        """Remove existing message, keeping the rest of the comments fields.
461        If comments contains 'subject: ', message will prepend
462        the field and a blank line."""
463        if self.message:
464            subj = b'subject: ' + self.message[0].lower()
465            for i in pycompat.xrange(len(self.comments)):
466                if subj == self.comments[i].lower():
467                    del self.comments[i]
468                    self.message = self.message[2:]
469                    break
470        ci = 0
471        for mi in self.message:
472            while mi != self.comments[ci]:
473                ci += 1
474            del self.comments[ci]
475
476
477def newcommit(repo, phase, *args, **kwargs):
478    """helper dedicated to ensure a commit respect mq.secret setting
479
480    It should be used instead of repo.commit inside the mq source for operation
481    creating new changeset.
482    """
483    repo = repo.unfiltered()
484    if phase is None:
485        if repo.ui.configbool(b'mq', b'secret'):
486            phase = phases.secret
487    overrides = {(b'ui', b'allowemptycommit'): True}
488    if phase is not None:
489        overrides[(b'phases', b'new-commit')] = phase
490    with repo.ui.configoverride(overrides, b'mq'):
491        repo.ui.setconfig(b'ui', b'allowemptycommit', True)
492        return repo.commit(*args, **kwargs)
493
494
495class AbortNoCleanup(error.Abort):
496    pass
497
498
499class queue(object):
500    def __init__(self, ui, baseui, path, patchdir=None):
501        self.basepath = path
502        try:
503            with open(os.path.join(path, b'patches.queue'), 'rb') as fh:
504                cur = fh.read().rstrip()
505
506            if not cur:
507                curpath = os.path.join(path, b'patches')
508            else:
509                curpath = os.path.join(path, b'patches-' + cur)
510        except IOError:
511            curpath = os.path.join(path, b'patches')
512        self.path = patchdir or curpath
513        self.opener = vfsmod.vfs(self.path)
514        self.ui = ui
515        self.baseui = baseui
516        self.applieddirty = False
517        self.seriesdirty = False
518        self.added = []
519        self.seriespath = b"series"
520        self.statuspath = b"status"
521        self.guardspath = b"guards"
522        self.activeguards = None
523        self.guardsdirty = False
524        # Handle mq.git as a bool with extended values
525        gitmode = ui.config(b'mq', b'git').lower()
526        boolmode = stringutil.parsebool(gitmode)
527        if boolmode is not None:
528            if boolmode:
529                gitmode = b'yes'
530            else:
531                gitmode = b'no'
532        self.gitmode = gitmode
533        # deprecated config: mq.plain
534        self.plainmode = ui.configbool(b'mq', b'plain')
535        self.checkapplied = True
536
537    @util.propertycache
538    def applied(self):
539        def parselines(lines):
540            for l in lines:
541                entry = l.split(b':', 1)
542                if len(entry) > 1:
543                    n, name = entry
544                    yield statusentry(bin(n), name)
545                elif l.strip():
546                    self.ui.warn(
547                        _(b'malformated mq status line: %s\n')
548                        % stringutil.pprint(entry)
549                    )
550                # else we ignore empty lines
551
552        try:
553            lines = self.opener.read(self.statuspath).splitlines()
554            return list(parselines(lines))
555        except IOError as e:
556            if e.errno == errno.ENOENT:
557                return []
558            raise
559
560    @util.propertycache
561    def fullseries(self):
562        try:
563            return self.opener.read(self.seriespath).splitlines()
564        except IOError as e:
565            if e.errno == errno.ENOENT:
566                return []
567            raise
568
569    @util.propertycache
570    def series(self):
571        self.parseseries()
572        return self.series
573
574    @util.propertycache
575    def seriesguards(self):
576        self.parseseries()
577        return self.seriesguards
578
579    def invalidate(self):
580        for a in 'applied fullseries series seriesguards'.split():
581            if a in self.__dict__:
582                delattr(self, a)
583        self.applieddirty = False
584        self.seriesdirty = False
585        self.guardsdirty = False
586        self.activeguards = None
587
588    def diffopts(self, opts=None, patchfn=None, plain=False):
589        """Return diff options tweaked for this mq use, possibly upgrading to
590        git format, and possibly plain and without lossy options."""
591        diffopts = patchmod.difffeatureopts(
592            self.ui,
593            opts,
594            git=True,
595            whitespace=not plain,
596            formatchanging=not plain,
597        )
598        if self.gitmode == b'auto':
599            diffopts.upgrade = True
600        elif self.gitmode == b'keep':
601            pass
602        elif self.gitmode in (b'yes', b'no'):
603            diffopts.git = self.gitmode == b'yes'
604        else:
605            raise error.Abort(
606                _(b'mq.git option can be auto/keep/yes/no got %s')
607                % self.gitmode
608            )
609        if patchfn:
610            diffopts = self.patchopts(diffopts, patchfn)
611        return diffopts
612
613    def patchopts(self, diffopts, *patches):
614        """Return a copy of input diff options with git set to true if
615        referenced patch is a git patch and should be preserved as such.
616        """
617        diffopts = diffopts.copy()
618        if not diffopts.git and self.gitmode == b'keep':
619            for patchfn in patches:
620                patchf = self.opener(patchfn, b'r')
621                # if the patch was a git patch, refresh it as a git patch
622                diffopts.git = any(
623                    line.startswith(b'diff --git') for line in patchf
624                )
625                patchf.close()
626        return diffopts
627
628    def join(self, *p):
629        return os.path.join(self.path, *p)
630
631    def findseries(self, patch):
632        def matchpatch(l):
633            l = l.split(b'#', 1)[0]
634            return l.strip() == patch
635
636        for index, l in enumerate(self.fullseries):
637            if matchpatch(l):
638                return index
639        return None
640
641    guard_re = re.compile(br'\s?#([-+][^-+# \t\r\n\f][^# \t\r\n\f]*)')
642
643    def parseseries(self):
644        self.series = []
645        self.seriesguards = []
646        for l in self.fullseries:
647            h = l.find(b'#')
648            if h == -1:
649                patch = l
650                comment = b''
651            elif h == 0:
652                continue
653            else:
654                patch = l[:h]
655                comment = l[h:]
656            patch = patch.strip()
657            if patch:
658                if patch in self.series:
659                    raise error.Abort(
660                        _(b'%s appears more than once in %s')
661                        % (patch, self.join(self.seriespath))
662                    )
663                self.series.append(patch)
664                self.seriesguards.append(self.guard_re.findall(comment))
665
666    def checkguard(self, guard):
667        if not guard:
668            return _(b'guard cannot be an empty string')
669        bad_chars = b'# \t\r\n\f'
670        first = guard[0]
671        if first in b'-+':
672            return _(b'guard %r starts with invalid character: %r') % (
673                guard,
674                first,
675            )
676        for c in bad_chars:
677            if c in guard:
678                return _(b'invalid character in guard %r: %r') % (guard, c)
679
680    def setactive(self, guards):
681        for guard in guards:
682            bad = self.checkguard(guard)
683            if bad:
684                raise error.Abort(bad)
685        guards = sorted(set(guards))
686        self.ui.debug(b'active guards: %s\n' % b' '.join(guards))
687        self.activeguards = guards
688        self.guardsdirty = True
689
690    def active(self):
691        if self.activeguards is None:
692            self.activeguards = []
693            try:
694                guards = self.opener.read(self.guardspath).split()
695            except IOError as err:
696                if err.errno != errno.ENOENT:
697                    raise
698                guards = []
699            for i, guard in enumerate(guards):
700                bad = self.checkguard(guard)
701                if bad:
702                    self.ui.warn(
703                        b'%s:%d: %s\n'
704                        % (self.join(self.guardspath), i + 1, bad)
705                    )
706                else:
707                    self.activeguards.append(guard)
708        return self.activeguards
709
710    def setguards(self, idx, guards):
711        for g in guards:
712            if len(g) < 2:
713                raise error.Abort(_(b'guard %r too short') % g)
714            if g[0] not in b'-+':
715                raise error.Abort(_(b'guard %r starts with invalid char') % g)
716            bad = self.checkguard(g[1:])
717            if bad:
718                raise error.Abort(bad)
719        drop = self.guard_re.sub(b'', self.fullseries[idx])
720        self.fullseries[idx] = drop + b''.join([b' #' + g for g in guards])
721        self.parseseries()
722        self.seriesdirty = True
723
724    def pushable(self, idx):
725        if isinstance(idx, bytes):
726            idx = self.series.index(idx)
727        patchguards = self.seriesguards[idx]
728        if not patchguards:
729            return True, None
730        guards = self.active()
731        exactneg = [
732            g for g in patchguards if g.startswith(b'-') and g[1:] in guards
733        ]
734        if exactneg:
735            return False, stringutil.pprint(exactneg[0])
736        pos = [g for g in patchguards if g.startswith(b'+')]
737        exactpos = [g for g in pos if g[1:] in guards]
738        if pos:
739            if exactpos:
740                return True, stringutil.pprint(exactpos[0])
741            return False, b' '.join([stringutil.pprint(p) for p in pos])
742        return True, b''
743
744    def explainpushable(self, idx, all_patches=False):
745        if all_patches:
746            write = self.ui.write
747        else:
748            write = self.ui.warn
749
750        if all_patches or self.ui.verbose:
751            if isinstance(idx, bytes):
752                idx = self.series.index(idx)
753            pushable, why = self.pushable(idx)
754            if all_patches and pushable:
755                if why is None:
756                    write(
757                        _(b'allowing %s - no guards in effect\n')
758                        % self.series[idx]
759                    )
760                else:
761                    if not why:
762                        write(
763                            _(b'allowing %s - no matching negative guards\n')
764                            % self.series[idx]
765                        )
766                    else:
767                        write(
768                            _(b'allowing %s - guarded by %s\n')
769                            % (self.series[idx], why)
770                        )
771            if not pushable:
772                if why:
773                    write(
774                        _(b'skipping %s - guarded by %s\n')
775                        % (self.series[idx], why)
776                    )
777                else:
778                    write(
779                        _(b'skipping %s - no matching guards\n')
780                        % self.series[idx]
781                    )
782
783    def savedirty(self):
784        def writelist(items, path):
785            fp = self.opener(path, b'wb')
786            for i in items:
787                fp.write(b"%s\n" % i)
788            fp.close()
789
790        if self.applieddirty:
791            writelist(map(bytes, self.applied), self.statuspath)
792            self.applieddirty = False
793        if self.seriesdirty:
794            writelist(self.fullseries, self.seriespath)
795            self.seriesdirty = False
796        if self.guardsdirty:
797            writelist(self.activeguards, self.guardspath)
798            self.guardsdirty = False
799        if self.added:
800            qrepo = self.qrepo()
801            if qrepo:
802                qrepo[None].add(f for f in self.added if f not in qrepo[None])
803            self.added = []
804
805    def removeundo(self, repo):
806        undo = repo.sjoin(b'undo')
807        if not os.path.exists(undo):
808            return
809        try:
810            os.unlink(undo)
811        except OSError as inst:
812            self.ui.warn(
813                _(b'error removing undo: %s\n') % stringutil.forcebytestr(inst)
814            )
815
816    def backup(self, repo, files, copy=False):
817        # backup local changes in --force case
818        for f in sorted(files):
819            absf = repo.wjoin(f)
820            if os.path.lexists(absf):
821                absorig = scmutil.backuppath(self.ui, repo, f)
822                self.ui.note(
823                    _(b'saving current version of %s as %s\n')
824                    % (f, os.path.relpath(absorig))
825                )
826
827                if copy:
828                    util.copyfile(absf, absorig)
829                else:
830                    util.rename(absf, absorig)
831
832    def printdiff(
833        self,
834        repo,
835        diffopts,
836        node1,
837        node2=None,
838        files=None,
839        fp=None,
840        changes=None,
841        opts=None,
842    ):
843        if opts is None:
844            opts = {}
845        stat = opts.get(b'stat')
846        m = scmutil.match(repo[node1], files, opts)
847        logcmdutil.diffordiffstat(
848            self.ui,
849            repo,
850            diffopts,
851            repo[node1],
852            repo[node2],
853            m,
854            changes,
855            stat,
856            fp,
857        )
858
859    def mergeone(self, repo, mergeq, head, patch, rev, diffopts):
860        # first try just applying the patch
861        (err, n) = self.apply(
862            repo, [patch], update_status=False, strict=True, merge=rev
863        )
864
865        if err == 0:
866            return (err, n)
867
868        if n is None:
869            raise error.Abort(_(b"apply failed for patch %s") % patch)
870
871        self.ui.warn(_(b"patch didn't work out, merging %s\n") % patch)
872
873        # apply failed, strip away that rev and merge.
874        hg.clean(repo, head)
875        strip(self.ui, repo, [n], update=False, backup=False)
876
877        ctx = repo[rev]
878        ret = hg.merge(ctx, remind=False)
879        if ret:
880            raise error.Abort(_(b"update returned %d") % ret)
881        n = newcommit(repo, None, ctx.description(), ctx.user(), force=True)
882        if n is None:
883            raise error.Abort(_(b"repo commit failed"))
884        try:
885            ph = patchheader(mergeq.join(patch), self.plainmode)
886        except Exception:
887            raise error.Abort(_(b"unable to read %s") % patch)
888
889        diffopts = self.patchopts(diffopts, patch)
890        patchf = self.opener(patch, b"w")
891        comments = bytes(ph)
892        if comments:
893            patchf.write(comments)
894        self.printdiff(repo, diffopts, head, n, fp=patchf)
895        patchf.close()
896        self.removeundo(repo)
897        return (0, n)
898
899    def qparents(self, repo, rev=None):
900        """return the mq handled parent or p1
901
902        In some case where mq get himself in being the parent of a merge the
903        appropriate parent may be p2.
904        (eg: an in progress merge started with mq disabled)
905
906        If no parent are managed by mq, p1 is returned.
907        """
908        if rev is None:
909            (p1, p2) = repo.dirstate.parents()
910            if p2 == repo.nullid:
911                return p1
912            if not self.applied:
913                return None
914            return self.applied[-1].node
915        p1, p2 = repo.changelog.parents(rev)
916        if p2 != repo.nullid and p2 in [x.node for x in self.applied]:
917            return p2
918        return p1
919
920    def mergepatch(self, repo, mergeq, series, diffopts):
921        if not self.applied:
922            # each of the patches merged in will have two parents.  This
923            # can confuse the qrefresh, qdiff, and strip code because it
924            # needs to know which parent is actually in the patch queue.
925            # so, we insert a merge marker with only one parent.  This way
926            # the first patch in the queue is never a merge patch
927            #
928            pname = b".hg.patches.merge.marker"
929            n = newcommit(repo, None, b'[mq]: merge marker', force=True)
930            self.removeundo(repo)
931            self.applied.append(statusentry(n, pname))
932            self.applieddirty = True
933
934        head = self.qparents(repo)
935
936        for patch in series:
937            patch = mergeq.lookup(patch, strict=True)
938            if not patch:
939                self.ui.warn(_(b"patch %s does not exist\n") % patch)
940                return (1, None)
941            pushable, reason = self.pushable(patch)
942            if not pushable:
943                self.explainpushable(patch, all_patches=True)
944                continue
945            info = mergeq.isapplied(patch)
946            if not info:
947                self.ui.warn(_(b"patch %s is not applied\n") % patch)
948                return (1, None)
949            rev = info[1]
950            err, head = self.mergeone(repo, mergeq, head, patch, rev, diffopts)
951            if head:
952                self.applied.append(statusentry(head, patch))
953                self.applieddirty = True
954            if err:
955                return (err, head)
956        self.savedirty()
957        return (0, head)
958
959    def patch(self, repo, patchfile):
960        """Apply patchfile  to the working directory.
961        patchfile: name of patch file"""
962        files = set()
963        try:
964            fuzz = patchmod.patch(
965                self.ui, repo, patchfile, strip=1, files=files, eolmode=None
966            )
967            return (True, list(files), fuzz)
968        except Exception as inst:
969            self.ui.note(stringutil.forcebytestr(inst) + b'\n')
970            if not self.ui.verbose:
971                self.ui.warn(_(b"patch failed, unable to continue (try -v)\n"))
972            self.ui.traceback()
973            return (False, list(files), False)
974
975    def apply(
976        self,
977        repo,
978        series,
979        list=False,
980        update_status=True,
981        strict=False,
982        patchdir=None,
983        merge=None,
984        all_files=None,
985        tobackup=None,
986        keepchanges=False,
987    ):
988        wlock = lock = tr = None
989        try:
990            wlock = repo.wlock()
991            lock = repo.lock()
992            tr = repo.transaction(b"qpush")
993            try:
994                ret = self._apply(
995                    repo,
996                    series,
997                    list,
998                    update_status,
999                    strict,
1000                    patchdir,
1001                    merge,
1002                    all_files=all_files,
1003                    tobackup=tobackup,
1004                    keepchanges=keepchanges,
1005                )
1006                tr.close()
1007                self.savedirty()
1008                return ret
1009            except AbortNoCleanup:
1010                tr.close()
1011                self.savedirty()
1012                raise
1013            except:  # re-raises
1014                try:
1015                    tr.abort()
1016                finally:
1017                    self.invalidate()
1018                raise
1019        finally:
1020            release(tr, lock, wlock)
1021            self.removeundo(repo)
1022
1023    def _apply(
1024        self,
1025        repo,
1026        series,
1027        list=False,
1028        update_status=True,
1029        strict=False,
1030        patchdir=None,
1031        merge=None,
1032        all_files=None,
1033        tobackup=None,
1034        keepchanges=False,
1035    ):
1036        """returns (error, hash)
1037
1038        error = 1 for unable to read, 2 for patch failed, 3 for patch
1039        fuzz. tobackup is None or a set of files to backup before they
1040        are modified by a patch.
1041        """
1042        # TODO unify with commands.py
1043        if not patchdir:
1044            patchdir = self.path
1045        err = 0
1046        n = None
1047        for patchname in series:
1048            pushable, reason = self.pushable(patchname)
1049            if not pushable:
1050                self.explainpushable(patchname, all_patches=True)
1051                continue
1052            self.ui.status(_(b"applying %s\n") % patchname)
1053            pf = os.path.join(patchdir, patchname)
1054
1055            try:
1056                ph = patchheader(self.join(patchname), self.plainmode)
1057            except IOError:
1058                self.ui.warn(_(b"unable to read %s\n") % patchname)
1059                err = 1
1060                break
1061
1062            message = ph.message
1063            if not message:
1064                # The commit message should not be translated
1065                message = b"imported patch %s\n" % patchname
1066            else:
1067                if list:
1068                    # The commit message should not be translated
1069                    message.append(b"\nimported patch %s" % patchname)
1070                message = b'\n'.join(message)
1071
1072            if ph.haspatch:
1073                if tobackup:
1074                    touched = patchmod.changedfiles(self.ui, repo, pf)
1075                    touched = set(touched) & tobackup
1076                    if touched and keepchanges:
1077                        raise AbortNoCleanup(
1078                            _(b"conflicting local changes found"),
1079                            hint=_(b"did you forget to qrefresh?"),
1080                        )
1081                    self.backup(repo, touched, copy=True)
1082                    tobackup = tobackup - touched
1083                (patcherr, files, fuzz) = self.patch(repo, pf)
1084                if all_files is not None:
1085                    all_files.update(files)
1086                patcherr = not patcherr
1087            else:
1088                self.ui.warn(_(b"patch %s is empty\n") % patchname)
1089                patcherr, files, fuzz = 0, [], 0
1090
1091            if merge and files:
1092                # Mark as removed/merged and update dirstate parent info
1093                with repo.dirstate.parentchange():
1094                    for f in files:
1095                        repo.dirstate.update_file_p1(f, p1_tracked=True)
1096                    p1 = repo.dirstate.p1()
1097                    repo.setparents(p1, merge)
1098
1099            if all_files and b'.hgsubstate' in all_files:
1100                wctx = repo[None]
1101                pctx = repo[b'.']
1102                overwrite = False
1103                mergedsubstate = subrepoutil.submerge(
1104                    repo, pctx, wctx, wctx, overwrite
1105                )
1106                files += mergedsubstate.keys()
1107
1108            match = scmutil.matchfiles(repo, files or [])
1109            oldtip = repo.changelog.tip()
1110            n = newcommit(
1111                repo, None, message, ph.user, ph.date, match=match, force=True
1112            )
1113            if repo.changelog.tip() == oldtip:
1114                raise error.Abort(
1115                    _(b"qpush exactly duplicates child changeset")
1116                )
1117            if n is None:
1118                raise error.Abort(_(b"repository commit failed"))
1119
1120            if update_status:
1121                self.applied.append(statusentry(n, patchname))
1122
1123            if patcherr:
1124                self.ui.warn(
1125                    _(b"patch failed, rejects left in working directory\n")
1126                )
1127                err = 2
1128                break
1129
1130            if fuzz and strict:
1131                self.ui.warn(_(b"fuzz found when applying patch, stopping\n"))
1132                err = 3
1133                break
1134        return (err, n)
1135
1136    def _cleanup(self, patches, numrevs, keep=False):
1137        if not keep:
1138            r = self.qrepo()
1139            if r:
1140                r[None].forget(patches)
1141            for p in patches:
1142                try:
1143                    os.unlink(self.join(p))
1144                except OSError as inst:
1145                    if inst.errno != errno.ENOENT:
1146                        raise
1147
1148        qfinished = []
1149        if numrevs:
1150            qfinished = self.applied[:numrevs]
1151            del self.applied[:numrevs]
1152            self.applieddirty = True
1153
1154        unknown = []
1155
1156        sortedseries = []
1157        for p in patches:
1158            idx = self.findseries(p)
1159            if idx is None:
1160                sortedseries.append((-1, p))
1161            else:
1162                sortedseries.append((idx, p))
1163
1164        sortedseries.sort(reverse=True)
1165        for (i, p) in sortedseries:
1166            if i != -1:
1167                del self.fullseries[i]
1168            else:
1169                unknown.append(p)
1170
1171        if unknown:
1172            if numrevs:
1173                rev = {entry.name: entry.node for entry in qfinished}
1174                for p in unknown:
1175                    msg = _(b'revision %s refers to unknown patches: %s\n')
1176                    self.ui.warn(msg % (short(rev[p]), p))
1177            else:
1178                msg = _(b'unknown patches: %s\n')
1179                raise error.Abort(b''.join(msg % p for p in unknown))
1180
1181        self.parseseries()
1182        self.seriesdirty = True
1183        return [entry.node for entry in qfinished]
1184
1185    def _revpatches(self, repo, revs):
1186        firstrev = repo[self.applied[0].node].rev()
1187        patches = []
1188        for i, rev in enumerate(revs):
1189
1190            if rev < firstrev:
1191                raise error.Abort(_(b'revision %d is not managed') % rev)
1192
1193            ctx = repo[rev]
1194            base = self.applied[i].node
1195            if ctx.node() != base:
1196                msg = _(b'cannot delete revision %d above applied patches')
1197                raise error.Abort(msg % rev)
1198
1199            patch = self.applied[i].name
1200            for fmt in (b'[mq]: %s', b'imported patch %s'):
1201                if ctx.description() == fmt % patch:
1202                    msg = _(b'patch %s finalized without changeset message\n')
1203                    repo.ui.status(msg % patch)
1204                    break
1205
1206            patches.append(patch)
1207        return patches
1208
1209    def finish(self, repo, revs):
1210        # Manually trigger phase computation to ensure phasedefaults is
1211        # executed before we remove the patches.
1212        repo._phasecache
1213        patches = self._revpatches(repo, sorted(revs))
1214        qfinished = self._cleanup(patches, len(patches))
1215        if qfinished and repo.ui.configbool(b'mq', b'secret'):
1216            # only use this logic when the secret option is added
1217            oldqbase = repo[qfinished[0]]
1218            tphase = phases.newcommitphase(repo.ui)
1219            if oldqbase.phase() > tphase and oldqbase.p1().phase() <= tphase:
1220                with repo.transaction(b'qfinish') as tr:
1221                    phases.advanceboundary(repo, tr, tphase, qfinished)
1222
1223    def delete(self, repo, patches, opts):
1224        if not patches and not opts.get(b'rev'):
1225            raise error.Abort(
1226                _(b'qdelete requires at least one revision or patch name')
1227            )
1228
1229        realpatches = []
1230        for patch in patches:
1231            patch = self.lookup(patch, strict=True)
1232            info = self.isapplied(patch)
1233            if info:
1234                raise error.Abort(_(b"cannot delete applied patch %s") % patch)
1235            if patch not in self.series:
1236                raise error.Abort(_(b"patch %s not in series file") % patch)
1237            if patch not in realpatches:
1238                realpatches.append(patch)
1239
1240        numrevs = 0
1241        if opts.get(b'rev'):
1242            if not self.applied:
1243                raise error.Abort(_(b'no patches applied'))
1244            revs = logcmdutil.revrange(repo, opts.get(b'rev'))
1245            revs.sort()
1246            revpatches = self._revpatches(repo, revs)
1247            realpatches += revpatches
1248            numrevs = len(revpatches)
1249
1250        self._cleanup(realpatches, numrevs, opts.get(b'keep'))
1251
1252    def checktoppatch(self, repo):
1253        '''check that working directory is at qtip'''
1254        if self.applied:
1255            top = self.applied[-1].node
1256            patch = self.applied[-1].name
1257            if repo.dirstate.p1() != top:
1258                raise error.Abort(_(b"working directory revision is not qtip"))
1259            return top, patch
1260        return None, None
1261
1262    def putsubstate2changes(self, substatestate, changes):
1263        if isinstance(changes, list):
1264            mar = changes[:3]
1265        else:
1266            mar = (changes.modified, changes.added, changes.removed)
1267        if any((b'.hgsubstate' in files for files in mar)):
1268            return  # already listed up
1269        # not yet listed up
1270        if substatestate.added or not substatestate.any_tracked:
1271            mar[1].append(b'.hgsubstate')
1272        elif substatestate.removed:
1273            mar[2].append(b'.hgsubstate')
1274        else:  # modified
1275            mar[0].append(b'.hgsubstate')
1276
1277    def checklocalchanges(self, repo, force=False, refresh=True):
1278        excsuffix = b''
1279        if refresh:
1280            excsuffix = b', qrefresh first'
1281            # plain versions for i18n tool to detect them
1282            _(b"local changes found, qrefresh first")
1283            _(b"local changed subrepos found, qrefresh first")
1284
1285        s = repo.status()
1286        if not force:
1287            cmdutil.checkunfinished(repo)
1288            if s.modified or s.added or s.removed or s.deleted:
1289                _(b"local changes found")  # i18n tool detection
1290                raise error.Abort(_(b"local changes found" + excsuffix))
1291            if checksubstate(repo):
1292                _(b"local changed subrepos found")  # i18n tool detection
1293                raise error.Abort(
1294                    _(b"local changed subrepos found" + excsuffix)
1295                )
1296        else:
1297            cmdutil.checkunfinished(repo, skipmerge=True)
1298        return s
1299
1300    _reserved = (b'series', b'status', b'guards', b'.', b'..')
1301
1302    def checkreservedname(self, name):
1303        if name in self._reserved:
1304            raise error.Abort(
1305                _(b'"%s" cannot be used as the name of a patch') % name
1306            )
1307        if name != name.strip():
1308            # whitespace is stripped by parseseries()
1309            raise error.Abort(
1310                _(b'patch name cannot begin or end with whitespace')
1311            )
1312        for prefix in (b'.hg', b'.mq'):
1313            if name.startswith(prefix):
1314                raise error.Abort(
1315                    _(b'patch name cannot begin with "%s"') % prefix
1316                )
1317        for c in (b'#', b':', b'\r', b'\n'):
1318            if c in name:
1319                raise error.Abort(
1320                    _(b'%r cannot be used in the name of a patch')
1321                    % pycompat.bytestr(c)
1322                )
1323
1324    def checkpatchname(self, name, force=False):
1325        self.checkreservedname(name)
1326        if not force and os.path.exists(self.join(name)):
1327            if os.path.isdir(self.join(name)):
1328                raise error.Abort(
1329                    _(b'"%s" already exists as a directory') % name
1330                )
1331            else:
1332                raise error.Abort(_(b'patch "%s" already exists') % name)
1333
1334    def makepatchname(self, title, fallbackname):
1335        """Return a suitable filename for title, adding a suffix to make
1336        it unique in the existing list"""
1337        namebase = re.sub(br'[\s\W_]+', b'_', title.lower()).strip(b'_')
1338        namebase = namebase[:75]  # avoid too long name (issue5117)
1339        if namebase:
1340            try:
1341                self.checkreservedname(namebase)
1342            except error.Abort:
1343                namebase = fallbackname
1344        else:
1345            namebase = fallbackname
1346        name = namebase
1347        i = 0
1348        while True:
1349            if name not in self.fullseries:
1350                try:
1351                    self.checkpatchname(name)
1352                    break
1353                except error.Abort:
1354                    pass
1355            i += 1
1356            name = b'%s__%d' % (namebase, i)
1357        return name
1358
1359    def checkkeepchanges(self, keepchanges, force):
1360        if force and keepchanges:
1361            raise error.Abort(_(b'cannot use both --force and --keep-changes'))
1362
1363    def new(self, repo, patchfn, *pats, **opts):
1364        """options:
1365        msg: a string or a no-argument function returning a string
1366        """
1367        opts = pycompat.byteskwargs(opts)
1368        msg = opts.get(b'msg')
1369        edit = opts.get(b'edit')
1370        editform = opts.get(b'editform', b'mq.qnew')
1371        user = opts.get(b'user')
1372        date = opts.get(b'date')
1373        if date:
1374            date = dateutil.parsedate(date)
1375        diffopts = self.diffopts({b'git': opts.get(b'git')}, plain=True)
1376        if opts.get(b'checkname', True):
1377            self.checkpatchname(patchfn)
1378        inclsubs = checksubstate(repo)
1379        if inclsubs:
1380            substatestate = repo.dirstate.get_entry(b'.hgsubstate')
1381        if opts.get(b'include') or opts.get(b'exclude') or pats:
1382            # detect missing files in pats
1383            def badfn(f, msg):
1384                if f != b'.hgsubstate':  # .hgsubstate is auto-created
1385                    raise error.Abort(b'%s: %s' % (f, msg))
1386
1387            match = scmutil.match(repo[None], pats, opts, badfn=badfn)
1388            changes = repo.status(match=match)
1389        else:
1390            changes = self.checklocalchanges(repo, force=True)
1391        commitfiles = list(inclsubs)
1392        commitfiles.extend(changes.modified)
1393        commitfiles.extend(changes.added)
1394        commitfiles.extend(changes.removed)
1395        match = scmutil.matchfiles(repo, commitfiles)
1396        if len(repo[None].parents()) > 1:
1397            raise error.Abort(_(b'cannot manage merge changesets'))
1398        self.checktoppatch(repo)
1399        insert = self.fullseriesend()
1400        with repo.wlock():
1401            try:
1402                # if patch file write fails, abort early
1403                p = self.opener(patchfn, b"w")
1404            except IOError as e:
1405                raise error.Abort(
1406                    _(b'cannot write patch "%s": %s')
1407                    % (patchfn, encoding.strtolocal(e.strerror))
1408                )
1409            try:
1410                defaultmsg = b"[mq]: %s" % patchfn
1411                editor = cmdutil.getcommiteditor(editform=editform)
1412                if edit:
1413
1414                    def finishdesc(desc):
1415                        if desc.rstrip():
1416                            return desc
1417                        else:
1418                            return defaultmsg
1419
1420                    # i18n: this message is shown in editor with "HG: " prefix
1421                    extramsg = _(b'Leave message empty to use default message.')
1422                    editor = cmdutil.getcommiteditor(
1423                        finishdesc=finishdesc,
1424                        extramsg=extramsg,
1425                        editform=editform,
1426                    )
1427                    commitmsg = msg
1428                else:
1429                    commitmsg = msg or defaultmsg
1430
1431                n = newcommit(
1432                    repo,
1433                    None,
1434                    commitmsg,
1435                    user,
1436                    date,
1437                    match=match,
1438                    force=True,
1439                    editor=editor,
1440                )
1441                if n is None:
1442                    raise error.Abort(_(b"repo commit failed"))
1443                try:
1444                    self.fullseries[insert:insert] = [patchfn]
1445                    self.applied.append(statusentry(n, patchfn))
1446                    self.parseseries()
1447                    self.seriesdirty = True
1448                    self.applieddirty = True
1449                    nctx = repo[n]
1450                    ph = patchheader(self.join(patchfn), self.plainmode)
1451                    if user:
1452                        ph.setuser(user)
1453                    if date:
1454                        ph.setdate(b'%d %d' % date)
1455                    ph.setparent(hex(nctx.p1().node()))
1456                    msg = nctx.description().strip()
1457                    if msg == defaultmsg.strip():
1458                        msg = b''
1459                    ph.setmessage(msg)
1460                    p.write(bytes(ph))
1461                    if commitfiles:
1462                        parent = self.qparents(repo, n)
1463                        if inclsubs:
1464                            self.putsubstate2changes(substatestate, changes)
1465                        chunks = patchmod.diff(
1466                            repo,
1467                            node1=parent,
1468                            node2=n,
1469                            changes=changes,
1470                            opts=diffopts,
1471                        )
1472                        for chunk in chunks:
1473                            p.write(chunk)
1474                    p.close()
1475                    r = self.qrepo()
1476                    if r:
1477                        r[None].add([patchfn])
1478                except:  # re-raises
1479                    repo.rollback()
1480                    raise
1481            except Exception:
1482                patchpath = self.join(patchfn)
1483                try:
1484                    os.unlink(patchpath)
1485                except OSError:
1486                    self.ui.warn(_(b'error unlinking %s\n') % patchpath)
1487                raise
1488            self.removeundo(repo)
1489
1490    def isapplied(self, patch):
1491        """returns (index, rev, patch)"""
1492        for i, a in enumerate(self.applied):
1493            if a.name == patch:
1494                return (i, a.node, a.name)
1495        return None
1496
1497    # if the exact patch name does not exist, we try a few
1498    # variations.  If strict is passed, we try only #1
1499    #
1500    # 1) a number (as string) to indicate an offset in the series file
1501    # 2) a unique substring of the patch name was given
1502    # 3) patchname[-+]num to indicate an offset in the series file
1503    def lookup(self, patch, strict=False):
1504        def partialname(s):
1505            if s in self.series:
1506                return s
1507            matches = [x for x in self.series if s in x]
1508            if len(matches) > 1:
1509                self.ui.warn(_(b'patch name "%s" is ambiguous:\n') % s)
1510                for m in matches:
1511                    self.ui.warn(b'  %s\n' % m)
1512                return None
1513            if matches:
1514                return matches[0]
1515            if self.series and self.applied:
1516                if s == b'qtip':
1517                    return self.series[self.seriesend(True) - 1]
1518                if s == b'qbase':
1519                    return self.series[0]
1520            return None
1521
1522        if patch in self.series:
1523            return patch
1524
1525        if not os.path.isfile(self.join(patch)):
1526            try:
1527                sno = int(patch)
1528            except (ValueError, OverflowError):
1529                pass
1530            else:
1531                if -len(self.series) <= sno < len(self.series):
1532                    return self.series[sno]
1533
1534            if not strict:
1535                res = partialname(patch)
1536                if res:
1537                    return res
1538                minus = patch.rfind(b'-')
1539                if minus >= 0:
1540                    res = partialname(patch[:minus])
1541                    if res:
1542                        i = self.series.index(res)
1543                        try:
1544                            off = int(patch[minus + 1 :] or 1)
1545                        except (ValueError, OverflowError):
1546                            pass
1547                        else:
1548                            if i - off >= 0:
1549                                return self.series[i - off]
1550                plus = patch.rfind(b'+')
1551                if plus >= 0:
1552                    res = partialname(patch[:plus])
1553                    if res:
1554                        i = self.series.index(res)
1555                        try:
1556                            off = int(patch[plus + 1 :] or 1)
1557                        except (ValueError, OverflowError):
1558                            pass
1559                        else:
1560                            if i + off < len(self.series):
1561                                return self.series[i + off]
1562        raise error.Abort(_(b"patch %s not in series") % patch)
1563
1564    def push(
1565        self,
1566        repo,
1567        patch=None,
1568        force=False,
1569        list=False,
1570        mergeq=None,
1571        all=False,
1572        move=False,
1573        exact=False,
1574        nobackup=False,
1575        keepchanges=False,
1576    ):
1577        self.checkkeepchanges(keepchanges, force)
1578        diffopts = self.diffopts()
1579        with repo.wlock():
1580            heads = []
1581            for hs in repo.branchmap().iterheads():
1582                heads.extend(hs)
1583            if not heads:
1584                heads = [repo.nullid]
1585            if repo.dirstate.p1() not in heads and not exact:
1586                self.ui.status(_(b"(working directory not at a head)\n"))
1587
1588            if not self.series:
1589                self.ui.warn(_(b'no patches in series\n'))
1590                return 0
1591
1592            # Suppose our series file is: A B C and the current 'top'
1593            # patch is B. qpush C should be performed (moving forward)
1594            # qpush B is a NOP (no change) qpush A is an error (can't
1595            # go backwards with qpush)
1596            if patch:
1597                patch = self.lookup(patch)
1598                info = self.isapplied(patch)
1599                if info and info[0] >= len(self.applied) - 1:
1600                    self.ui.warn(
1601                        _(b'qpush: %s is already at the top\n') % patch
1602                    )
1603                    return 0
1604
1605                pushable, reason = self.pushable(patch)
1606                if pushable:
1607                    if self.series.index(patch) < self.seriesend():
1608                        raise error.Abort(
1609                            _(b"cannot push to a previous patch: %s") % patch
1610                        )
1611                else:
1612                    if reason:
1613                        reason = _(b'guarded by %s') % reason
1614                    else:
1615                        reason = _(b'no matching guards')
1616                    self.ui.warn(
1617                        _(b"cannot push '%s' - %s\n") % (patch, reason)
1618                    )
1619                    return 1
1620            elif all:
1621                patch = self.series[-1]
1622                if self.isapplied(patch):
1623                    self.ui.warn(_(b'all patches are currently applied\n'))
1624                    return 0
1625
1626            # Following the above example, starting at 'top' of B:
1627            # qpush should be performed (pushes C), but a subsequent
1628            # qpush without an argument is an error (nothing to
1629            # apply). This allows a loop of "...while hg qpush..." to
1630            # work as it detects an error when done
1631            start = self.seriesend()
1632            if start == len(self.series):
1633                self.ui.warn(_(b'patch series already fully applied\n'))
1634                return 1
1635            if not force and not keepchanges:
1636                self.checklocalchanges(repo, refresh=self.applied)
1637
1638            if exact:
1639                if keepchanges:
1640                    raise error.Abort(
1641                        _(b"cannot use --exact and --keep-changes together")
1642                    )
1643                if move:
1644                    raise error.Abort(
1645                        _(b'cannot use --exact and --move together')
1646                    )
1647                if self.applied:
1648                    raise error.Abort(
1649                        _(b'cannot push --exact with applied patches')
1650                    )
1651                root = self.series[start]
1652                target = patchheader(self.join(root), self.plainmode).parent
1653                if not target:
1654                    raise error.Abort(
1655                        _(b"%s does not have a parent recorded") % root
1656                    )
1657                if not repo[target] == repo[b'.']:
1658                    hg.update(repo, target)
1659
1660            if move:
1661                if not patch:
1662                    raise error.Abort(_(b"please specify the patch to move"))
1663                for fullstart, rpn in enumerate(self.fullseries):
1664                    # strip markers for patch guards
1665                    if self.guard_re.split(rpn, 1)[0] == self.series[start]:
1666                        break
1667                for i, rpn in enumerate(self.fullseries[fullstart:]):
1668                    # strip markers for patch guards
1669                    if self.guard_re.split(rpn, 1)[0] == patch:
1670                        break
1671                index = fullstart + i
1672                assert index < len(self.fullseries)
1673                fullpatch = self.fullseries[index]
1674                del self.fullseries[index]
1675                self.fullseries.insert(fullstart, fullpatch)
1676                self.parseseries()
1677                self.seriesdirty = True
1678
1679            self.applieddirty = True
1680            if start > 0:
1681                self.checktoppatch(repo)
1682            if not patch:
1683                patch = self.series[start]
1684                end = start + 1
1685            else:
1686                end = self.series.index(patch, start) + 1
1687
1688            tobackup = set()
1689            if (not nobackup and force) or keepchanges:
1690                status = self.checklocalchanges(repo, force=True)
1691                if keepchanges:
1692                    tobackup.update(
1693                        status.modified
1694                        + status.added
1695                        + status.removed
1696                        + status.deleted
1697                    )
1698                else:
1699                    tobackup.update(status.modified + status.added)
1700
1701            s = self.series[start:end]
1702            all_files = set()
1703            try:
1704                if mergeq:
1705                    ret = self.mergepatch(repo, mergeq, s, diffopts)
1706                else:
1707                    ret = self.apply(
1708                        repo,
1709                        s,
1710                        list,
1711                        all_files=all_files,
1712                        tobackup=tobackup,
1713                        keepchanges=keepchanges,
1714                    )
1715            except AbortNoCleanup:
1716                raise
1717            except:  # re-raises
1718                self.ui.warn(_(b'cleaning up working directory...\n'))
1719                cmdutil.revert(
1720                    self.ui,
1721                    repo,
1722                    repo[b'.'],
1723                    no_backup=True,
1724                )
1725                # only remove unknown files that we know we touched or
1726                # created while patching
1727                for f in all_files:
1728                    if f not in repo.dirstate:
1729                        repo.wvfs.unlinkpath(f, ignoremissing=True)
1730                self.ui.warn(_(b'done\n'))
1731                raise
1732
1733            if not self.applied:
1734                return ret[0]
1735            top = self.applied[-1].name
1736            if ret[0] and ret[0] > 1:
1737                msg = _(b"errors during apply, please fix and qrefresh %s\n")
1738                self.ui.write(msg % top)
1739            else:
1740                self.ui.write(_(b"now at: %s\n") % top)
1741            return ret[0]
1742
1743    def pop(
1744        self,
1745        repo,
1746        patch=None,
1747        force=False,
1748        update=True,
1749        all=False,
1750        nobackup=False,
1751        keepchanges=False,
1752    ):
1753        self.checkkeepchanges(keepchanges, force)
1754        with repo.wlock():
1755            if patch:
1756                # index, rev, patch
1757                info = self.isapplied(patch)
1758                if not info:
1759                    patch = self.lookup(patch)
1760                info = self.isapplied(patch)
1761                if not info:
1762                    raise error.Abort(_(b"patch %s is not applied") % patch)
1763
1764            if not self.applied:
1765                # Allow qpop -a to work repeatedly,
1766                # but not qpop without an argument
1767                self.ui.warn(_(b"no patches applied\n"))
1768                return not all
1769
1770            if all:
1771                start = 0
1772            elif patch:
1773                start = info[0] + 1
1774            else:
1775                start = len(self.applied) - 1
1776
1777            if start >= len(self.applied):
1778                self.ui.warn(_(b"qpop: %s is already at the top\n") % patch)
1779                return
1780
1781            if not update:
1782                parents = repo.dirstate.parents()
1783                rr = [x.node for x in self.applied]
1784                for p in parents:
1785                    if p in rr:
1786                        self.ui.warn(_(b"qpop: forcing dirstate update\n"))
1787                        update = True
1788            else:
1789                parents = [p.node() for p in repo[None].parents()]
1790                update = any(
1791                    entry.node in parents for entry in self.applied[start:]
1792                )
1793
1794            tobackup = set()
1795            if update:
1796                s = self.checklocalchanges(repo, force=force or keepchanges)
1797                if force:
1798                    if not nobackup:
1799                        tobackup.update(s.modified + s.added)
1800                elif keepchanges:
1801                    tobackup.update(
1802                        s.modified + s.added + s.removed + s.deleted
1803                    )
1804
1805            self.applieddirty = True
1806            end = len(self.applied)
1807            rev = self.applied[start].node
1808
1809            try:
1810                heads = repo.changelog.heads(rev)
1811            except error.LookupError:
1812                node = short(rev)
1813                raise error.Abort(_(b'trying to pop unknown node %s') % node)
1814
1815            if heads != [self.applied[-1].node]:
1816                raise error.Abort(
1817                    _(
1818                        b"popping would remove a revision not "
1819                        b"managed by this patch queue"
1820                    )
1821                )
1822            if not repo[self.applied[-1].node].mutable():
1823                raise error.Abort(
1824                    _(b"popping would remove a public revision"),
1825                    hint=_(b"see 'hg help phases' for details"),
1826                )
1827
1828            # we know there are no local changes, so we can make a simplified
1829            # form of hg.update.
1830            if update:
1831                qp = self.qparents(repo, rev)
1832                ctx = repo[qp]
1833                st = repo.status(qp, b'.')
1834                m, a, r, d = st.modified, st.added, st.removed, st.deleted
1835                if d:
1836                    raise error.Abort(_(b"deletions found between repo revs"))
1837
1838                tobackup = set(a + m + r) & tobackup
1839                if keepchanges and tobackup:
1840                    raise error.Abort(_(b"local changes found, qrefresh first"))
1841                self.backup(repo, tobackup)
1842                with repo.dirstate.parentchange():
1843                    for f in a:
1844                        repo.wvfs.unlinkpath(f, ignoremissing=True)
1845                        repo.dirstate.update_file(
1846                            f, p1_tracked=False, wc_tracked=False
1847                        )
1848                    for f in m + r:
1849                        fctx = ctx[f]
1850                        repo.wwrite(f, fctx.data(), fctx.flags())
1851                        repo.dirstate.update_file(
1852                            f, p1_tracked=True, wc_tracked=True
1853                        )
1854                    repo.setparents(qp, repo.nullid)
1855            for patch in reversed(self.applied[start:end]):
1856                self.ui.status(_(b"popping %s\n") % patch.name)
1857            del self.applied[start:end]
1858            strip(self.ui, repo, [rev], update=False, backup=False)
1859            for s, state in repo[b'.'].substate.items():
1860                repo[b'.'].sub(s).get(state)
1861            if self.applied:
1862                self.ui.write(_(b"now at: %s\n") % self.applied[-1].name)
1863            else:
1864                self.ui.write(_(b"patch queue now empty\n"))
1865
1866    def diff(self, repo, pats, opts):
1867        top, patch = self.checktoppatch(repo)
1868        if not top:
1869            self.ui.write(_(b"no patches applied\n"))
1870            return
1871        qp = self.qparents(repo, top)
1872        if opts.get(b'reverse'):
1873            node1, node2 = None, qp
1874        else:
1875            node1, node2 = qp, None
1876        diffopts = self.diffopts(opts, patch)
1877        self.printdiff(repo, diffopts, node1, node2, files=pats, opts=opts)
1878
1879    def refresh(self, repo, pats=None, **opts):
1880        opts = pycompat.byteskwargs(opts)
1881        if not self.applied:
1882            self.ui.write(_(b"no patches applied\n"))
1883            return 1
1884        msg = opts.get(b'msg', b'').rstrip()
1885        edit = opts.get(b'edit')
1886        editform = opts.get(b'editform', b'mq.qrefresh')
1887        newuser = opts.get(b'user')
1888        newdate = opts.get(b'date')
1889        if newdate:
1890            newdate = b'%d %d' % dateutil.parsedate(newdate)
1891        wlock = repo.wlock()
1892
1893        try:
1894            self.checktoppatch(repo)
1895            (top, patchfn) = (self.applied[-1].node, self.applied[-1].name)
1896            if repo.changelog.heads(top) != [top]:
1897                raise error.Abort(
1898                    _(b"cannot qrefresh a revision with children")
1899                )
1900            if not repo[top].mutable():
1901                raise error.Abort(
1902                    _(b"cannot qrefresh public revision"),
1903                    hint=_(b"see 'hg help phases' for details"),
1904                )
1905
1906            cparents = repo.changelog.parents(top)
1907            patchparent = self.qparents(repo, top)
1908
1909            inclsubs = checksubstate(repo, patchparent)
1910            if inclsubs:
1911                substatestate = repo.dirstate.get_entry(b'.hgsubstate')
1912
1913            ph = patchheader(self.join(patchfn), self.plainmode)
1914            diffopts = self.diffopts(
1915                {b'git': opts.get(b'git')}, patchfn, plain=True
1916            )
1917            if newuser:
1918                ph.setuser(newuser)
1919            if newdate:
1920                ph.setdate(newdate)
1921            ph.setparent(hex(patchparent))
1922
1923            # only commit new patch when write is complete
1924            patchf = self.opener(patchfn, b'w', atomictemp=True)
1925
1926            # update the dirstate in place, strip off the qtip commit
1927            # and then commit.
1928            #
1929            # this should really read:
1930            #   st = repo.status(top, patchparent)
1931            # but we do it backwards to take advantage of manifest/changelog
1932            # caching against the next repo.status call
1933            st = repo.status(patchparent, top)
1934            mm, aa, dd = st.modified, st.added, st.removed
1935            ctx = repo[top]
1936            aaa = aa[:]
1937            match1 = scmutil.match(repo[None], pats, opts)
1938            # in short mode, we only diff the files included in the
1939            # patch already plus specified files
1940            if opts.get(b'short'):
1941                # if amending a patch, we start with existing
1942                # files plus specified files - unfiltered
1943                match = scmutil.matchfiles(repo, mm + aa + dd + match1.files())
1944                # filter with include/exclude options
1945                match1 = scmutil.match(repo[None], opts=opts)
1946            else:
1947                match = scmutil.matchall(repo)
1948            stb = repo.status(match=match)
1949            m, a, r, d = stb.modified, stb.added, stb.removed, stb.deleted
1950            mm = set(mm)
1951            aa = set(aa)
1952            dd = set(dd)
1953
1954            # we might end up with files that were added between
1955            # qtip and the dirstate parent, but then changed in the
1956            # local dirstate. in this case, we want them to only
1957            # show up in the added section
1958            for x in m:
1959                if x not in aa:
1960                    mm.add(x)
1961            # we might end up with files added by the local dirstate that
1962            # were deleted by the patch.  In this case, they should only
1963            # show up in the changed section.
1964            for x in a:
1965                if x in dd:
1966                    dd.remove(x)
1967                    mm.add(x)
1968                else:
1969                    aa.add(x)
1970            # make sure any files deleted in the local dirstate
1971            # are not in the add or change column of the patch
1972            forget = []
1973            for x in d + r:
1974                if x in aa:
1975                    aa.remove(x)
1976                    forget.append(x)
1977                    continue
1978                else:
1979                    mm.discard(x)
1980                dd.add(x)
1981
1982            m = list(mm)
1983            r = list(dd)
1984            a = list(aa)
1985
1986            # create 'match' that includes the files to be recommitted.
1987            # apply match1 via repo.status to ensure correct case handling.
1988            st = repo.status(patchparent, match=match1)
1989            cm, ca, cr, cd = st.modified, st.added, st.removed, st.deleted
1990            allmatches = set(cm + ca + cr + cd)
1991            refreshchanges = [x.intersection(allmatches) for x in (mm, aa, dd)]
1992
1993            files = set(inclsubs)
1994            for x in refreshchanges:
1995                files.update(x)
1996            match = scmutil.matchfiles(repo, files)
1997
1998            bmlist = repo[top].bookmarks()
1999
2000            with repo.dirstate.parentchange():
2001                # XXX do we actually need the dirstateguard
2002                dsguard = None
2003                try:
2004                    dsguard = dirstateguard.dirstateguard(repo, b'mq.refresh')
2005                    if diffopts.git or diffopts.upgrade:
2006                        copies = {}
2007                        for dst in a:
2008                            src = repo.dirstate.copied(dst)
2009                            # during qfold, the source file for copies may
2010                            # be removed. Treat this as a simple add.
2011                            if src is not None and src in repo.dirstate:
2012                                copies.setdefault(src, []).append(dst)
2013                            repo.dirstate.update_file(
2014                                dst, p1_tracked=False, wc_tracked=True
2015                            )
2016                        # remember the copies between patchparent and qtip
2017                        for dst in aaa:
2018                            src = ctx[dst].copysource()
2019                            if src:
2020                                copies.setdefault(src, []).extend(
2021                                    copies.get(dst, [])
2022                                )
2023                                if dst in a:
2024                                    copies[src].append(dst)
2025                            # we can't copy a file created by the patch itself
2026                            if dst in copies:
2027                                del copies[dst]
2028                        for src, dsts in pycompat.iteritems(copies):
2029                            for dst in dsts:
2030                                repo.dirstate.copy(src, dst)
2031                    else:
2032                        for dst in a:
2033                            repo.dirstate.update_file(
2034                                dst, p1_tracked=False, wc_tracked=True
2035                            )
2036                        # Drop useless copy information
2037                        for f in list(repo.dirstate.copies()):
2038                            repo.dirstate.copy(None, f)
2039                    for f in r:
2040                        repo.dirstate.update_file_p1(f, p1_tracked=True)
2041                    # if the patch excludes a modified file, mark that
2042                    # file with mtime=0 so status can see it.
2043                    mm = []
2044                    for i in pycompat.xrange(len(m) - 1, -1, -1):
2045                        if not match1(m[i]):
2046                            mm.append(m[i])
2047                            del m[i]
2048                    for f in m:
2049                        repo.dirstate.update_file_p1(f, p1_tracked=True)
2050                    for f in mm:
2051                        repo.dirstate.update_file_p1(f, p1_tracked=True)
2052                    for f in forget:
2053                        repo.dirstate.update_file_p1(f, p1_tracked=False)
2054
2055                    user = ph.user or ctx.user()
2056
2057                    oldphase = repo[top].phase()
2058
2059                    # assumes strip can roll itself back if interrupted
2060                    repo.setparents(*cparents)
2061                    self.applied.pop()
2062                    self.applieddirty = True
2063                    strip(self.ui, repo, [top], update=False, backup=False)
2064                    dsguard.close()
2065                finally:
2066                    release(dsguard)
2067
2068            try:
2069                # might be nice to attempt to roll back strip after this
2070
2071                defaultmsg = b"[mq]: %s" % patchfn
2072                editor = cmdutil.getcommiteditor(editform=editform)
2073                if edit:
2074
2075                    def finishdesc(desc):
2076                        if desc.rstrip():
2077                            ph.setmessage(desc)
2078                            return desc
2079                        return defaultmsg
2080
2081                    # i18n: this message is shown in editor with "HG: " prefix
2082                    extramsg = _(b'Leave message empty to use default message.')
2083                    editor = cmdutil.getcommiteditor(
2084                        finishdesc=finishdesc,
2085                        extramsg=extramsg,
2086                        editform=editform,
2087                    )
2088                    message = msg or b"\n".join(ph.message)
2089                elif not msg:
2090                    if not ph.message:
2091                        message = defaultmsg
2092                    else:
2093                        message = b"\n".join(ph.message)
2094                else:
2095                    message = msg
2096                    ph.setmessage(msg)
2097
2098                # Ensure we create a new changeset in the same phase than
2099                # the old one.
2100                lock = tr = None
2101                try:
2102                    lock = repo.lock()
2103                    tr = repo.transaction(b'mq')
2104                    n = newcommit(
2105                        repo,
2106                        oldphase,
2107                        message,
2108                        user,
2109                        ph.date,
2110                        match=match,
2111                        force=True,
2112                        editor=editor,
2113                    )
2114                    # only write patch after a successful commit
2115                    c = [list(x) for x in refreshchanges]
2116                    if inclsubs:
2117                        self.putsubstate2changes(substatestate, c)
2118                    chunks = patchmod.diff(
2119                        repo, patchparent, changes=c, opts=diffopts
2120                    )
2121                    comments = bytes(ph)
2122                    if comments:
2123                        patchf.write(comments)
2124                    for chunk in chunks:
2125                        patchf.write(chunk)
2126                    patchf.close()
2127
2128                    marks = repo._bookmarks
2129                    marks.applychanges(repo, tr, [(bm, n) for bm in bmlist])
2130                    tr.close()
2131
2132                    self.applied.append(statusentry(n, patchfn))
2133                finally:
2134                    lockmod.release(tr, lock)
2135            except:  # re-raises
2136                ctx = repo[cparents[0]]
2137                repo.dirstate.rebuild(ctx.node(), ctx.manifest())
2138                self.savedirty()
2139                self.ui.warn(
2140                    _(
2141                        b'qrefresh interrupted while patch was popped! '
2142                        b'(revert --all, qpush to recover)\n'
2143                    )
2144                )
2145                raise
2146        finally:
2147            wlock.release()
2148            self.removeundo(repo)
2149
2150    def init(self, repo, create=False):
2151        if not create and os.path.isdir(self.path):
2152            raise error.Abort(_(b"patch queue directory already exists"))
2153        try:
2154            os.mkdir(self.path)
2155        except OSError as inst:
2156            if inst.errno != errno.EEXIST or not create:
2157                raise
2158        if create:
2159            return self.qrepo(create=True)
2160
2161    def unapplied(self, repo, patch=None):
2162        if patch and patch not in self.series:
2163            raise error.Abort(_(b"patch %s is not in series file") % patch)
2164        if not patch:
2165            start = self.seriesend()
2166        else:
2167            start = self.series.index(patch) + 1
2168        unapplied = []
2169        for i in pycompat.xrange(start, len(self.series)):
2170            pushable, reason = self.pushable(i)
2171            if pushable:
2172                unapplied.append((i, self.series[i]))
2173            self.explainpushable(i)
2174        return unapplied
2175
2176    def qseries(
2177        self,
2178        repo,
2179        missing=None,
2180        start=0,
2181        length=None,
2182        status=None,
2183        summary=False,
2184    ):
2185        def displayname(pfx, patchname, state):
2186            if pfx:
2187                self.ui.write(pfx)
2188            if summary:
2189                ph = patchheader(self.join(patchname), self.plainmode)
2190                if ph.message:
2191                    msg = ph.message[0]
2192                else:
2193                    msg = b''
2194
2195                if self.ui.formatted():
2196                    width = self.ui.termwidth() - len(pfx) - len(patchname) - 2
2197                    if width > 0:
2198                        msg = stringutil.ellipsis(msg, width)
2199                    else:
2200                        msg = b''
2201                self.ui.write(patchname, label=b'qseries.' + state)
2202                self.ui.write(b': ')
2203                self.ui.write(msg, label=b'qseries.message.' + state)
2204            else:
2205                self.ui.write(patchname, label=b'qseries.' + state)
2206            self.ui.write(b'\n')
2207
2208        applied = {p.name for p in self.applied}
2209        if length is None:
2210            length = len(self.series) - start
2211        if not missing:
2212            if self.ui.verbose:
2213                idxwidth = len(b"%d" % (start + length - 1))
2214            for i in pycompat.xrange(start, start + length):
2215                patch = self.series[i]
2216                if patch in applied:
2217                    char, state = b'A', b'applied'
2218                elif self.pushable(i)[0]:
2219                    char, state = b'U', b'unapplied'
2220                else:
2221                    char, state = b'G', b'guarded'
2222                pfx = b''
2223                if self.ui.verbose:
2224                    pfx = b'%*d %s ' % (idxwidth, i, char)
2225                elif status and status != char:
2226                    continue
2227                displayname(pfx, patch, state)
2228        else:
2229            msng_list = []
2230            for root, dirs, files in os.walk(self.path):
2231                d = root[len(self.path) + 1 :]
2232                for f in files:
2233                    fl = os.path.join(d, f)
2234                    if (
2235                        fl not in self.series
2236                        and fl
2237                        not in (
2238                            self.statuspath,
2239                            self.seriespath,
2240                            self.guardspath,
2241                        )
2242                        and not fl.startswith(b'.')
2243                    ):
2244                        msng_list.append(fl)
2245            for x in sorted(msng_list):
2246                pfx = self.ui.verbose and b'D ' or b''
2247                displayname(pfx, x, b'missing')
2248
2249    def issaveline(self, l):
2250        if l.name == b'.hg.patches.save.line':
2251            return True
2252
2253    def qrepo(self, create=False):
2254        ui = self.baseui.copy()
2255        # copy back attributes set by ui.pager()
2256        if self.ui.pageractive and not ui.pageractive:
2257            ui.pageractive = self.ui.pageractive
2258            # internal config: ui.formatted
2259            ui.setconfig(
2260                b'ui',
2261                b'formatted',
2262                self.ui.config(b'ui', b'formatted'),
2263                b'mqpager',
2264            )
2265            ui.setconfig(
2266                b'ui',
2267                b'interactive',
2268                self.ui.config(b'ui', b'interactive'),
2269                b'mqpager',
2270            )
2271        if create or os.path.isdir(self.join(b".hg")):
2272            return hg.repository(ui, path=self.path, create=create)
2273
2274    def restore(self, repo, rev, delete=None, qupdate=None):
2275        desc = repo[rev].description().strip()
2276        lines = desc.splitlines()
2277        datastart = None
2278        series = []
2279        applied = []
2280        qpp = None
2281        for i, line in enumerate(lines):
2282            if line == b'Patch Data:':
2283                datastart = i + 1
2284            elif line.startswith(b'Dirstate:'):
2285                l = line.rstrip()
2286                l = l[10:].split(b' ')
2287                qpp = [bin(x) for x in l]
2288            elif datastart is not None:
2289                l = line.rstrip()
2290                n, name = l.split(b':', 1)
2291                if n:
2292                    applied.append(statusentry(bin(n), name))
2293                else:
2294                    series.append(l)
2295        if datastart is None:
2296            self.ui.warn(_(b"no saved patch data found\n"))
2297            return 1
2298        self.ui.warn(_(b"restoring status: %s\n") % lines[0])
2299        self.fullseries = series
2300        self.applied = applied
2301        self.parseseries()
2302        self.seriesdirty = True
2303        self.applieddirty = True
2304        heads = repo.changelog.heads()
2305        if delete:
2306            if rev not in heads:
2307                self.ui.warn(_(b"save entry has children, leaving it alone\n"))
2308            else:
2309                self.ui.warn(_(b"removing save entry %s\n") % short(rev))
2310                pp = repo.dirstate.parents()
2311                if rev in pp:
2312                    update = True
2313                else:
2314                    update = False
2315                strip(self.ui, repo, [rev], update=update, backup=False)
2316        if qpp:
2317            self.ui.warn(
2318                _(b"saved queue repository parents: %s %s\n")
2319                % (short(qpp[0]), short(qpp[1]))
2320            )
2321            if qupdate:
2322                self.ui.status(_(b"updating queue directory\n"))
2323                r = self.qrepo()
2324                if not r:
2325                    self.ui.warn(_(b"unable to load queue repository\n"))
2326                    return 1
2327                hg.clean(r, qpp[0])
2328
2329    def save(self, repo, msg=None):
2330        if not self.applied:
2331            self.ui.warn(_(b"save: no patches applied, exiting\n"))
2332            return 1
2333        if self.issaveline(self.applied[-1]):
2334            self.ui.warn(_(b"status is already saved\n"))
2335            return 1
2336
2337        if not msg:
2338            msg = _(b"hg patches saved state")
2339        else:
2340            msg = b"hg patches: " + msg.rstrip(b'\r\n')
2341        r = self.qrepo()
2342        if r:
2343            pp = r.dirstate.parents()
2344            msg += b"\nDirstate: %s %s" % (hex(pp[0]), hex(pp[1]))
2345        msg += b"\n\nPatch Data:\n"
2346        msg += b''.join(b'%s\n' % x for x in self.applied)
2347        msg += b''.join(b':%s\n' % x for x in self.fullseries)
2348        n = repo.commit(msg, force=True)
2349        if not n:
2350            self.ui.warn(_(b"repo commit failed\n"))
2351            return 1
2352        self.applied.append(statusentry(n, b'.hg.patches.save.line'))
2353        self.applieddirty = True
2354        self.removeundo(repo)
2355
2356    def fullseriesend(self):
2357        if self.applied:
2358            p = self.applied[-1].name
2359            end = self.findseries(p)
2360            if end is None:
2361                return len(self.fullseries)
2362            return end + 1
2363        return 0
2364
2365    def seriesend(self, all_patches=False):
2366        """If all_patches is False, return the index of the next pushable patch
2367        in the series, or the series length. If all_patches is True, return the
2368        index of the first patch past the last applied one.
2369        """
2370        end = 0
2371
2372        def nextpatch(start):
2373            if all_patches or start >= len(self.series):
2374                return start
2375            for i in pycompat.xrange(start, len(self.series)):
2376                p, reason = self.pushable(i)
2377                if p:
2378                    return i
2379                self.explainpushable(i)
2380            return len(self.series)
2381
2382        if self.applied:
2383            p = self.applied[-1].name
2384            try:
2385                end = self.series.index(p)
2386            except ValueError:
2387                return 0
2388            return nextpatch(end + 1)
2389        return nextpatch(end)
2390
2391    def appliedname(self, index):
2392        pname = self.applied[index].name
2393        if not self.ui.verbose:
2394            p = pname
2395        else:
2396            p = (b"%d" % self.series.index(pname)) + b" " + pname
2397        return p
2398
2399    def qimport(
2400        self,
2401        repo,
2402        files,
2403        patchname=None,
2404        rev=None,
2405        existing=None,
2406        force=None,
2407        git=False,
2408    ):
2409        def checkseries(patchname):
2410            if patchname in self.series:
2411                raise error.Abort(
2412                    _(b'patch %s is already in the series file') % patchname
2413                )
2414
2415        if rev:
2416            if files:
2417                raise error.Abort(
2418                    _(b'option "-r" not valid when importing files')
2419                )
2420            rev = logcmdutil.revrange(repo, rev)
2421            rev.sort(reverse=True)
2422        elif not files:
2423            raise error.Abort(_(b'no files or revisions specified'))
2424        if (len(files) > 1 or len(rev) > 1) and patchname:
2425            raise error.Abort(
2426                _(b'option "-n" not valid when importing multiple patches')
2427            )
2428        imported = []
2429        if rev:
2430            # If mq patches are applied, we can only import revisions
2431            # that form a linear path to qbase.
2432            # Otherwise, they should form a linear path to a head.
2433            heads = repo.changelog.heads(repo.changelog.node(rev.first()))
2434            if len(heads) > 1:
2435                raise error.Abort(
2436                    _(b'revision %d is the root of more than one branch')
2437                    % rev.last()
2438                )
2439            if self.applied:
2440                base = repo.changelog.node(rev.first())
2441                if base in [n.node for n in self.applied]:
2442                    raise error.Abort(
2443                        _(b'revision %d is already managed') % rev.first()
2444                    )
2445                if heads != [self.applied[-1].node]:
2446                    raise error.Abort(
2447                        _(b'revision %d is not the parent of the queue')
2448                        % rev.first()
2449                    )
2450                base = repo.changelog.rev(self.applied[0].node)
2451                lastparent = repo.changelog.parentrevs(base)[0]
2452            else:
2453                if heads != [repo.changelog.node(rev.first())]:
2454                    raise error.Abort(
2455                        _(b'revision %d has unmanaged children') % rev.first()
2456                    )
2457                lastparent = None
2458
2459            diffopts = self.diffopts({b'git': git})
2460            with repo.transaction(b'qimport') as tr:
2461                for r in rev:
2462                    if not repo[r].mutable():
2463                        raise error.Abort(
2464                            _(b'revision %d is not mutable') % r,
2465                            hint=_(b"see 'hg help phases' " b'for details'),
2466                        )
2467                    p1, p2 = repo.changelog.parentrevs(r)
2468                    n = repo.changelog.node(r)
2469                    if p2 != nullrev:
2470                        raise error.Abort(
2471                            _(b'cannot import merge revision %d') % r
2472                        )
2473                    if lastparent and lastparent != r:
2474                        raise error.Abort(
2475                            _(b'revision %d is not the parent of %d')
2476                            % (r, lastparent)
2477                        )
2478                    lastparent = p1
2479
2480                    if not patchname:
2481                        patchname = self.makepatchname(
2482                            repo[r].description().split(b'\n', 1)[0],
2483                            b'%d.diff' % r,
2484                        )
2485                    checkseries(patchname)
2486                    self.checkpatchname(patchname, force)
2487                    self.fullseries.insert(0, patchname)
2488
2489                    with self.opener(patchname, b"w") as fp:
2490                        cmdutil.exportfile(repo, [n], fp, opts=diffopts)
2491
2492                    se = statusentry(n, patchname)
2493                    self.applied.insert(0, se)
2494
2495                    self.added.append(patchname)
2496                    imported.append(patchname)
2497                    patchname = None
2498                    if rev and repo.ui.configbool(b'mq', b'secret'):
2499                        # if we added anything with --rev, move the secret root
2500                        phases.retractboundary(repo, tr, phases.secret, [n])
2501                    self.parseseries()
2502                    self.applieddirty = True
2503                    self.seriesdirty = True
2504
2505        for i, filename in enumerate(files):
2506            if existing:
2507                if filename == b'-':
2508                    raise error.Abort(
2509                        _(b'-e is incompatible with import from -')
2510                    )
2511                filename = normname(filename)
2512                self.checkreservedname(filename)
2513                if urlutil.url(filename).islocal():
2514                    originpath = self.join(filename)
2515                    if not os.path.isfile(originpath):
2516                        raise error.Abort(
2517                            _(b"patch %s does not exist") % filename
2518                        )
2519
2520                if patchname:
2521                    self.checkpatchname(patchname, force)
2522
2523                    self.ui.write(
2524                        _(b'renaming %s to %s\n') % (filename, patchname)
2525                    )
2526                    util.rename(originpath, self.join(patchname))
2527                else:
2528                    patchname = filename
2529
2530            else:
2531                if filename == b'-' and not patchname:
2532                    raise error.Abort(
2533                        _(b'need --name to import a patch from -')
2534                    )
2535                elif not patchname:
2536                    patchname = normname(
2537                        os.path.basename(filename.rstrip(b'/'))
2538                    )
2539                self.checkpatchname(patchname, force)
2540                try:
2541                    if filename == b'-':
2542                        text = self.ui.fin.read()
2543                    else:
2544                        fp = hg.openpath(self.ui, filename)
2545                        text = fp.read()
2546                        fp.close()
2547                except (OSError, IOError):
2548                    raise error.Abort(_(b"unable to read file %s") % filename)
2549                patchf = self.opener(patchname, b"w")
2550                patchf.write(text)
2551                patchf.close()
2552            if not force:
2553                checkseries(patchname)
2554            if patchname not in self.series:
2555                index = self.fullseriesend() + i
2556                self.fullseries[index:index] = [patchname]
2557            self.parseseries()
2558            self.seriesdirty = True
2559            self.ui.warn(_(b"adding %s to series file\n") % patchname)
2560            self.added.append(patchname)
2561            imported.append(patchname)
2562            patchname = None
2563
2564        self.removeundo(repo)
2565        return imported
2566
2567
2568def fixkeepchangesopts(ui, opts):
2569    if (
2570        not ui.configbool(b'mq', b'keepchanges')
2571        or opts.get(b'force')
2572        or opts.get(b'exact')
2573    ):
2574        return opts
2575    opts = dict(opts)
2576    opts[b'keep_changes'] = True
2577    return opts
2578
2579
2580@command(
2581    b"qdelete|qremove|qrm",
2582    [
2583        (b'k', b'keep', None, _(b'keep patch file')),
2584        (
2585            b'r',
2586            b'rev',
2587            [],
2588            _(b'stop managing a revision (DEPRECATED)'),
2589            _(b'REV'),
2590        ),
2591    ],
2592    _(b'hg qdelete [-k] [PATCH]...'),
2593    helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
2594)
2595def delete(ui, repo, *patches, **opts):
2596    """remove patches from queue
2597
2598    The patches must not be applied, and at least one patch is required. Exact
2599    patch identifiers must be given. With -k/--keep, the patch files are
2600    preserved in the patch directory.
2601
2602    To stop managing a patch and move it into permanent history,
2603    use the :hg:`qfinish` command."""
2604    q = repo.mq
2605    q.delete(repo, patches, pycompat.byteskwargs(opts))
2606    q.savedirty()
2607    return 0
2608
2609
2610@command(
2611    b"qapplied",
2612    [(b'1', b'last', None, _(b'show only the preceding applied patch'))]
2613    + seriesopts,
2614    _(b'hg qapplied [-1] [-s] [PATCH]'),
2615    helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
2616)
2617def applied(ui, repo, patch=None, **opts):
2618    """print the patches already applied
2619
2620    Returns 0 on success."""
2621
2622    q = repo.mq
2623    opts = pycompat.byteskwargs(opts)
2624
2625    if patch:
2626        if patch not in q.series:
2627            raise error.Abort(_(b"patch %s is not in series file") % patch)
2628        end = q.series.index(patch) + 1
2629    else:
2630        end = q.seriesend(True)
2631
2632    if opts.get(b'last') and not end:
2633        ui.write(_(b"no patches applied\n"))
2634        return 1
2635    elif opts.get(b'last') and end == 1:
2636        ui.write(_(b"only one patch applied\n"))
2637        return 1
2638    elif opts.get(b'last'):
2639        start = end - 2
2640        end = 1
2641    else:
2642        start = 0
2643
2644    q.qseries(
2645        repo, length=end, start=start, status=b'A', summary=opts.get(b'summary')
2646    )
2647
2648
2649@command(
2650    b"qunapplied",
2651    [(b'1', b'first', None, _(b'show only the first patch'))] + seriesopts,
2652    _(b'hg qunapplied [-1] [-s] [PATCH]'),
2653    helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
2654)
2655def unapplied(ui, repo, patch=None, **opts):
2656    """print the patches not yet applied
2657
2658    Returns 0 on success."""
2659
2660    q = repo.mq
2661    opts = pycompat.byteskwargs(opts)
2662    if patch:
2663        if patch not in q.series:
2664            raise error.Abort(_(b"patch %s is not in series file") % patch)
2665        start = q.series.index(patch) + 1
2666    else:
2667        start = q.seriesend(True)
2668
2669    if start == len(q.series) and opts.get(b'first'):
2670        ui.write(_(b"all patches applied\n"))
2671        return 1
2672
2673    if opts.get(b'first'):
2674        length = 1
2675    else:
2676        length = None
2677    q.qseries(
2678        repo,
2679        start=start,
2680        length=length,
2681        status=b'U',
2682        summary=opts.get(b'summary'),
2683    )
2684
2685
2686@command(
2687    b"qimport",
2688    [
2689        (b'e', b'existing', None, _(b'import file in patch directory')),
2690        (b'n', b'name', b'', _(b'name of patch file'), _(b'NAME')),
2691        (b'f', b'force', None, _(b'overwrite existing files')),
2692        (
2693            b'r',
2694            b'rev',
2695            [],
2696            _(b'place existing revisions under mq control'),
2697            _(b'REV'),
2698        ),
2699        (b'g', b'git', None, _(b'use git extended diff format')),
2700        (b'P', b'push', None, _(b'qpush after importing')),
2701    ],
2702    _(b'hg qimport [-e] [-n NAME] [-f] [-g] [-P] [-r REV]... [FILE]...'),
2703    helpcategory=command.CATEGORY_IMPORT_EXPORT,
2704)
2705def qimport(ui, repo, *filename, **opts):
2706    """import a patch or existing changeset
2707
2708    The patch is inserted into the series after the last applied
2709    patch. If no patches have been applied, qimport prepends the patch
2710    to the series.
2711
2712    The patch will have the same name as its source file unless you
2713    give it a new one with -n/--name.
2714
2715    You can register an existing patch inside the patch directory with
2716    the -e/--existing flag.
2717
2718    With -f/--force, an existing patch of the same name will be
2719    overwritten.
2720
2721    An existing changeset may be placed under mq control with -r/--rev
2722    (e.g. qimport --rev . -n patch will place the current revision
2723    under mq control). With -g/--git, patches imported with --rev will
2724    use the git diff format. See the diffs help topic for information
2725    on why this is important for preserving rename/copy information
2726    and permission changes. Use :hg:`qfinish` to remove changesets
2727    from mq control.
2728
2729    To import a patch from standard input, pass - as the patch file.
2730    When importing from standard input, a patch name must be specified
2731    using the --name flag.
2732
2733    To import an existing patch while renaming it::
2734
2735      hg qimport -e existing-patch -n new-name
2736
2737    Returns 0 if import succeeded.
2738    """
2739    opts = pycompat.byteskwargs(opts)
2740    with repo.lock():  # cause this may move phase
2741        q = repo.mq
2742        try:
2743            imported = q.qimport(
2744                repo,
2745                filename,
2746                patchname=opts.get(b'name'),
2747                existing=opts.get(b'existing'),
2748                force=opts.get(b'force'),
2749                rev=opts.get(b'rev'),
2750                git=opts.get(b'git'),
2751            )
2752        finally:
2753            q.savedirty()
2754
2755    if imported and opts.get(b'push') and not opts.get(b'rev'):
2756        return q.push(repo, imported[-1])
2757    return 0
2758
2759
2760def qinit(ui, repo, create):
2761    """initialize a new queue repository
2762
2763    This command also creates a series file for ordering patches, and
2764    an mq-specific .hgignore file in the queue repository, to exclude
2765    the status and guards files (these contain mostly transient state).
2766
2767    Returns 0 if initialization succeeded."""
2768    q = repo.mq
2769    r = q.init(repo, create)
2770    q.savedirty()
2771    if r:
2772        if not os.path.exists(r.wjoin(b'.hgignore')):
2773            fp = r.wvfs(b'.hgignore', b'w')
2774            fp.write(b'^\\.hg\n')
2775            fp.write(b'^\\.mq\n')
2776            fp.write(b'syntax: glob\n')
2777            fp.write(b'status\n')
2778            fp.write(b'guards\n')
2779            fp.close()
2780        if not os.path.exists(r.wjoin(b'series')):
2781            r.wvfs(b'series', b'w').close()
2782        r[None].add([b'.hgignore', b'series'])
2783        commands.add(ui, r)
2784    return 0
2785
2786
2787@command(
2788    b"qinit",
2789    [(b'c', b'create-repo', None, _(b'create queue repository'))],
2790    _(b'hg qinit [-c]'),
2791    helpcategory=command.CATEGORY_REPO_CREATION,
2792    helpbasic=True,
2793)
2794def init(ui, repo, **opts):
2795    """init a new queue repository (DEPRECATED)
2796
2797    The queue repository is unversioned by default. If
2798    -c/--create-repo is specified, qinit will create a separate nested
2799    repository for patches (qinit -c may also be run later to convert
2800    an unversioned patch repository into a versioned one). You can use
2801    qcommit to commit changes to this queue repository.
2802
2803    This command is deprecated. Without -c, it's implied by other relevant
2804    commands. With -c, use :hg:`init --mq` instead."""
2805    return qinit(ui, repo, create=opts.get('create_repo'))
2806
2807
2808@command(
2809    b"qclone",
2810    [
2811        (b'', b'pull', None, _(b'use pull protocol to copy metadata')),
2812        (
2813            b'U',
2814            b'noupdate',
2815            None,
2816            _(b'do not update the new working directories'),
2817        ),
2818        (
2819            b'',
2820            b'uncompressed',
2821            None,
2822            _(b'use uncompressed transfer (fast over LAN)'),
2823        ),
2824        (
2825            b'p',
2826            b'patches',
2827            b'',
2828            _(b'location of source patch repository'),
2829            _(b'REPO'),
2830        ),
2831    ]
2832    + cmdutil.remoteopts,
2833    _(b'hg qclone [OPTION]... SOURCE [DEST]'),
2834    helpcategory=command.CATEGORY_REPO_CREATION,
2835    norepo=True,
2836)
2837def clone(ui, source, dest=None, **opts):
2838    """clone main and patch repository at same time
2839
2840    If source is local, destination will have no patches applied. If
2841    source is remote, this command can not check if patches are
2842    applied in source, so cannot guarantee that patches are not
2843    applied in destination. If you clone remote repository, be sure
2844    before that it has no patches applied.
2845
2846    Source patch repository is looked for in <src>/.hg/patches by
2847    default. Use -p <url> to change.
2848
2849    The patch directory must be a nested Mercurial repository, as
2850    would be created by :hg:`init --mq`.
2851
2852    Return 0 on success.
2853    """
2854    opts = pycompat.byteskwargs(opts)
2855
2856    def patchdir(repo):
2857        """compute a patch repo url from a repo object"""
2858        url = repo.url()
2859        if url.endswith(b'/'):
2860            url = url[:-1]
2861        return url + b'/.hg/patches'
2862
2863    # main repo (destination and sources)
2864    if dest is None:
2865        dest = hg.defaultdest(source)
2866    __, source_path, __ = urlutil.get_clone_path(ui, source)
2867    sr = hg.peer(ui, opts, source_path)
2868
2869    # patches repo (source only)
2870    if opts.get(b'patches'):
2871        __, patchespath, __ = urlutil.get_clone_path(ui, opts.get(b'patches'))
2872    else:
2873        patchespath = patchdir(sr)
2874    try:
2875        hg.peer(ui, opts, patchespath)
2876    except error.RepoError:
2877        raise error.Abort(
2878            _(b'versioned patch repository not found (see init --mq)')
2879        )
2880    qbase, destrev = None, None
2881    if sr.local():
2882        repo = sr.local()
2883        if repo.mq.applied and repo[qbase].phase() != phases.secret:
2884            qbase = repo.mq.applied[0].node
2885            if not hg.islocal(dest):
2886                heads = set(repo.heads())
2887                destrev = list(heads.difference(repo.heads(qbase)))
2888                destrev.append(repo.changelog.parents(qbase)[0])
2889    elif sr.capable(b'lookup'):
2890        try:
2891            qbase = sr.lookup(b'qbase')
2892        except error.RepoError:
2893            pass
2894
2895    ui.note(_(b'cloning main repository\n'))
2896    sr, dr = hg.clone(
2897        ui,
2898        opts,
2899        sr.url(),
2900        dest,
2901        pull=opts.get(b'pull'),
2902        revs=destrev,
2903        update=False,
2904        stream=opts.get(b'uncompressed'),
2905    )
2906
2907    ui.note(_(b'cloning patch repository\n'))
2908    hg.clone(
2909        ui,
2910        opts,
2911        opts.get(b'patches') or patchdir(sr),
2912        patchdir(dr),
2913        pull=opts.get(b'pull'),
2914        update=not opts.get(b'noupdate'),
2915        stream=opts.get(b'uncompressed'),
2916    )
2917
2918    if dr.local():
2919        repo = dr.local()
2920        if qbase:
2921            ui.note(
2922                _(
2923                    b'stripping applied patches from destination '
2924                    b'repository\n'
2925                )
2926            )
2927            strip(ui, repo, [qbase], update=False, backup=None)
2928        if not opts.get(b'noupdate'):
2929            ui.note(_(b'updating destination repository\n'))
2930            hg.update(repo, repo.changelog.tip())
2931
2932
2933@command(
2934    b"qcommit|qci",
2935    commands.table[b"commit|ci"][1],
2936    _(b'hg qcommit [OPTION]... [FILE]...'),
2937    helpcategory=command.CATEGORY_COMMITTING,
2938    inferrepo=True,
2939)
2940def commit(ui, repo, *pats, **opts):
2941    """commit changes in the queue repository (DEPRECATED)
2942
2943    This command is deprecated; use :hg:`commit --mq` instead."""
2944    q = repo.mq
2945    r = q.qrepo()
2946    if not r:
2947        raise error.Abort(b'no queue repository')
2948    commands.commit(r.ui, r, *pats, **opts)
2949
2950
2951@command(
2952    b"qseries",
2953    [
2954        (b'm', b'missing', None, _(b'print patches not in series')),
2955    ]
2956    + seriesopts,
2957    _(b'hg qseries [-ms]'),
2958    helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
2959)
2960def series(ui, repo, **opts):
2961    """print the entire series file
2962
2963    Returns 0 on success."""
2964    repo.mq.qseries(
2965        repo, missing=opts.get('missing'), summary=opts.get('summary')
2966    )
2967    return 0
2968
2969
2970@command(
2971    b"qtop",
2972    seriesopts,
2973    _(b'hg qtop [-s]'),
2974    helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
2975)
2976def top(ui, repo, **opts):
2977    """print the name of the current patch
2978
2979    Returns 0 on success."""
2980    q = repo.mq
2981    if q.applied:
2982        t = q.seriesend(True)
2983    else:
2984        t = 0
2985
2986    if t:
2987        q.qseries(
2988            repo,
2989            start=t - 1,
2990            length=1,
2991            status=b'A',
2992            summary=opts.get('summary'),
2993        )
2994    else:
2995        ui.write(_(b"no patches applied\n"))
2996        return 1
2997
2998
2999@command(
3000    b"qnext",
3001    seriesopts,
3002    _(b'hg qnext [-s]'),
3003    helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
3004)
3005def next(ui, repo, **opts):
3006    """print the name of the next pushable patch
3007
3008    Returns 0 on success."""
3009    q = repo.mq
3010    end = q.seriesend()
3011    if end == len(q.series):
3012        ui.write(_(b"all patches applied\n"))
3013        return 1
3014    q.qseries(repo, start=end, length=1, summary=opts.get('summary'))
3015
3016
3017@command(
3018    b"qprev",
3019    seriesopts,
3020    _(b'hg qprev [-s]'),
3021    helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
3022)
3023def prev(ui, repo, **opts):
3024    """print the name of the preceding applied patch
3025
3026    Returns 0 on success."""
3027    q = repo.mq
3028    l = len(q.applied)
3029    if l == 1:
3030        ui.write(_(b"only one patch applied\n"))
3031        return 1
3032    if not l:
3033        ui.write(_(b"no patches applied\n"))
3034        return 1
3035    idx = q.series.index(q.applied[-2].name)
3036    q.qseries(
3037        repo, start=idx, length=1, status=b'A', summary=opts.get('summary')
3038    )
3039
3040
3041def setupheaderopts(ui, opts):
3042    if not opts.get(b'user') and opts.get(b'currentuser'):
3043        opts[b'user'] = ui.username()
3044    if not opts.get(b'date') and opts.get(b'currentdate'):
3045        opts[b'date'] = b"%d %d" % dateutil.makedate()
3046
3047
3048@command(
3049    b"qnew",
3050    [
3051        (b'e', b'edit', None, _(b'invoke editor on commit messages')),
3052        (b'f', b'force', None, _(b'import uncommitted changes (DEPRECATED)')),
3053        (b'g', b'git', None, _(b'use git extended diff format')),
3054        (b'U', b'currentuser', None, _(b'add "From: <current user>" to patch')),
3055        (b'u', b'user', b'', _(b'add "From: <USER>" to patch'), _(b'USER')),
3056        (b'D', b'currentdate', None, _(b'add "Date: <current date>" to patch')),
3057        (b'd', b'date', b'', _(b'add "Date: <DATE>" to patch'), _(b'DATE')),
3058    ]
3059    + cmdutil.walkopts
3060    + cmdutil.commitopts,
3061    _(b'hg qnew [-e] [-m TEXT] [-l FILE] PATCH [FILE]...'),
3062    helpcategory=command.CATEGORY_COMMITTING,
3063    helpbasic=True,
3064    inferrepo=True,
3065)
3066def new(ui, repo, patch, *args, **opts):
3067    """create a new patch
3068
3069    qnew creates a new patch on top of the currently-applied patch (if
3070    any). The patch will be initialized with any outstanding changes
3071    in the working directory. You may also use -I/--include,
3072    -X/--exclude, and/or a list of files after the patch name to add
3073    only changes to matching files to the new patch, leaving the rest
3074    as uncommitted modifications.
3075
3076    -u/--user and -d/--date can be used to set the (given) user and
3077    date, respectively. -U/--currentuser and -D/--currentdate set user
3078    to current user and date to current date.
3079
3080    -e/--edit, -m/--message or -l/--logfile set the patch header as
3081    well as the commit message. If none is specified, the header is
3082    empty and the commit message is '[mq]: PATCH'.
3083
3084    Use the -g/--git option to keep the patch in the git extended diff
3085    format. Read the diffs help topic for more information on why this
3086    is important for preserving permission changes and copy/rename
3087    information.
3088
3089    Returns 0 on successful creation of a new patch.
3090    """
3091    opts = pycompat.byteskwargs(opts)
3092    msg = cmdutil.logmessage(ui, opts)
3093    q = repo.mq
3094    opts[b'msg'] = msg
3095    setupheaderopts(ui, opts)
3096    q.new(repo, patch, *args, **pycompat.strkwargs(opts))
3097    q.savedirty()
3098    return 0
3099
3100
3101@command(
3102    b"qrefresh",
3103    [
3104        (b'e', b'edit', None, _(b'invoke editor on commit messages')),
3105        (b'g', b'git', None, _(b'use git extended diff format')),
3106        (
3107            b's',
3108            b'short',
3109            None,
3110            _(b'refresh only files already in the patch and specified files'),
3111        ),
3112        (
3113            b'U',
3114            b'currentuser',
3115            None,
3116            _(b'add/update author field in patch with current user'),
3117        ),
3118        (
3119            b'u',
3120            b'user',
3121            b'',
3122            _(b'add/update author field in patch with given user'),
3123            _(b'USER'),
3124        ),
3125        (
3126            b'D',
3127            b'currentdate',
3128            None,
3129            _(b'add/update date field in patch with current date'),
3130        ),
3131        (
3132            b'd',
3133            b'date',
3134            b'',
3135            _(b'add/update date field in patch with given date'),
3136            _(b'DATE'),
3137        ),
3138    ]
3139    + cmdutil.walkopts
3140    + cmdutil.commitopts,
3141    _(b'hg qrefresh [-I] [-X] [-e] [-m TEXT] [-l FILE] [-s] [FILE]...'),
3142    helpcategory=command.CATEGORY_COMMITTING,
3143    helpbasic=True,
3144    inferrepo=True,
3145)
3146def refresh(ui, repo, *pats, **opts):
3147    """update the current patch
3148
3149    If any file patterns are provided, the refreshed patch will
3150    contain only the modifications that match those patterns; the
3151    remaining modifications will remain in the working directory.
3152
3153    If -s/--short is specified, files currently included in the patch
3154    will be refreshed just like matched files and remain in the patch.
3155
3156    If -e/--edit is specified, Mercurial will start your configured editor for
3157    you to enter a message. In case qrefresh fails, you will find a backup of
3158    your message in ``.hg/last-message.txt``.
3159
3160    hg add/remove/copy/rename work as usual, though you might want to
3161    use git-style patches (-g/--git or [diff] git=1) to track copies
3162    and renames. See the diffs help topic for more information on the
3163    git diff format.
3164
3165    Returns 0 on success.
3166    """
3167    opts = pycompat.byteskwargs(opts)
3168    q = repo.mq
3169    message = cmdutil.logmessage(ui, opts)
3170    setupheaderopts(ui, opts)
3171    with repo.wlock():
3172        ret = q.refresh(repo, pats, msg=message, **pycompat.strkwargs(opts))
3173        q.savedirty()
3174        return ret
3175
3176
3177@command(
3178    b"qdiff",
3179    cmdutil.diffopts + cmdutil.diffopts2 + cmdutil.walkopts,
3180    _(b'hg qdiff [OPTION]... [FILE]...'),
3181    helpcategory=command.CATEGORY_FILE_CONTENTS,
3182    helpbasic=True,
3183    inferrepo=True,
3184)
3185def diff(ui, repo, *pats, **opts):
3186    """diff of the current patch and subsequent modifications
3187
3188    Shows a diff which includes the current patch as well as any
3189    changes which have been made in the working directory since the
3190    last refresh (thus showing what the current patch would become
3191    after a qrefresh).
3192
3193    Use :hg:`diff` if you only want to see the changes made since the
3194    last qrefresh, or :hg:`export qtip` if you want to see changes
3195    made by the current patch without including changes made since the
3196    qrefresh.
3197
3198    Returns 0 on success.
3199    """
3200    ui.pager(b'qdiff')
3201    repo.mq.diff(repo, pats, pycompat.byteskwargs(opts))
3202    return 0
3203
3204
3205@command(
3206    b'qfold',
3207    [
3208        (b'e', b'edit', None, _(b'invoke editor on commit messages')),
3209        (b'k', b'keep', None, _(b'keep folded patch files')),
3210    ]
3211    + cmdutil.commitopts,
3212    _(b'hg qfold [-e] [-k] [-m TEXT] [-l FILE] PATCH...'),
3213    helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
3214)
3215def fold(ui, repo, *files, **opts):
3216    """fold the named patches into the current patch
3217
3218    Patches must not yet be applied. Each patch will be successively
3219    applied to the current patch in the order given. If all the
3220    patches apply successfully, the current patch will be refreshed
3221    with the new cumulative patch, and the folded patches will be
3222    deleted. With -k/--keep, the folded patch files will not be
3223    removed afterwards.
3224
3225    The header for each folded patch will be concatenated with the
3226    current patch header, separated by a line of ``* * *``.
3227
3228    Returns 0 on success."""
3229    opts = pycompat.byteskwargs(opts)
3230    q = repo.mq
3231    if not files:
3232        raise error.Abort(_(b'qfold requires at least one patch name'))
3233    if not q.checktoppatch(repo)[0]:
3234        raise error.Abort(_(b'no patches applied'))
3235    q.checklocalchanges(repo)
3236
3237    message = cmdutil.logmessage(ui, opts)
3238
3239    parent = q.lookup(b'qtip')
3240    patches = []
3241    messages = []
3242    for f in files:
3243        p = q.lookup(f)
3244        if p in patches or p == parent:
3245            ui.warn(_(b'skipping already folded patch %s\n') % p)
3246        if q.isapplied(p):
3247            raise error.Abort(
3248                _(b'qfold cannot fold already applied patch %s') % p
3249            )
3250        patches.append(p)
3251
3252    for p in patches:
3253        if not message:
3254            ph = patchheader(q.join(p), q.plainmode)
3255            if ph.message:
3256                messages.append(ph.message)
3257        pf = q.join(p)
3258        (patchsuccess, files, fuzz) = q.patch(repo, pf)
3259        if not patchsuccess:
3260            raise error.Abort(_(b'error folding patch %s') % p)
3261
3262    if not message:
3263        ph = patchheader(q.join(parent), q.plainmode)
3264        message = ph.message
3265        for msg in messages:
3266            if msg:
3267                if message:
3268                    message.append(b'* * *')
3269                message.extend(msg)
3270        message = b'\n'.join(message)
3271
3272    diffopts = q.patchopts(q.diffopts(), *patches)
3273    with repo.wlock():
3274        q.refresh(
3275            repo,
3276            msg=message,
3277            git=diffopts.git,
3278            edit=opts.get(b'edit'),
3279            editform=b'mq.qfold',
3280        )
3281        q.delete(repo, patches, opts)
3282        q.savedirty()
3283
3284
3285@command(
3286    b"qgoto",
3287    [
3288        (
3289            b'',
3290            b'keep-changes',
3291            None,
3292            _(b'tolerate non-conflicting local changes'),
3293        ),
3294        (b'f', b'force', None, _(b'overwrite any local changes')),
3295        (b'', b'no-backup', None, _(b'do not save backup copies of files')),
3296    ],
3297    _(b'hg qgoto [OPTION]... PATCH'),
3298    helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
3299)
3300def goto(ui, repo, patch, **opts):
3301    """push or pop patches until named patch is at top of stack
3302
3303    Returns 0 on success."""
3304    opts = pycompat.byteskwargs(opts)
3305    opts = fixkeepchangesopts(ui, opts)
3306    q = repo.mq
3307    patch = q.lookup(patch)
3308    nobackup = opts.get(b'no_backup')
3309    keepchanges = opts.get(b'keep_changes')
3310    if q.isapplied(patch):
3311        ret = q.pop(
3312            repo,
3313            patch,
3314            force=opts.get(b'force'),
3315            nobackup=nobackup,
3316            keepchanges=keepchanges,
3317        )
3318    else:
3319        ret = q.push(
3320            repo,
3321            patch,
3322            force=opts.get(b'force'),
3323            nobackup=nobackup,
3324            keepchanges=keepchanges,
3325        )
3326    q.savedirty()
3327    return ret
3328
3329
3330@command(
3331    b"qguard",
3332    [
3333        (b'l', b'list', None, _(b'list all patches and guards')),
3334        (b'n', b'none', None, _(b'drop all guards')),
3335    ],
3336    _(b'hg qguard [-l] [-n] [PATCH] [-- [+GUARD]... [-GUARD]...]'),
3337    helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
3338)
3339def guard(ui, repo, *args, **opts):
3340    """set or print guards for a patch
3341
3342    Guards control whether a patch can be pushed. A patch with no
3343    guards is always pushed. A patch with a positive guard ("+foo") is
3344    pushed only if the :hg:`qselect` command has activated it. A patch with
3345    a negative guard ("-foo") is never pushed if the :hg:`qselect` command
3346    has activated it.
3347
3348    With no arguments, print the currently active guards.
3349    With arguments, set guards for the named patch.
3350
3351    .. note::
3352
3353       Specifying negative guards now requires '--'.
3354
3355    To set guards on another patch::
3356
3357      hg qguard other.patch -- +2.6.17 -stable
3358
3359    Returns 0 on success.
3360    """
3361
3362    def status(idx):
3363        guards = q.seriesguards[idx] or [b'unguarded']
3364        if q.series[idx] in applied:
3365            state = b'applied'
3366        elif q.pushable(idx)[0]:
3367            state = b'unapplied'
3368        else:
3369            state = b'guarded'
3370        label = b'qguard.patch qguard.%s qseries.%s' % (state, state)
3371        ui.write(b'%s: ' % ui.label(q.series[idx], label))
3372
3373        for i, guard in enumerate(guards):
3374            if guard.startswith(b'+'):
3375                ui.write(guard, label=b'qguard.positive')
3376            elif guard.startswith(b'-'):
3377                ui.write(guard, label=b'qguard.negative')
3378            else:
3379                ui.write(guard, label=b'qguard.unguarded')
3380            if i != len(guards) - 1:
3381                ui.write(b' ')
3382        ui.write(b'\n')
3383
3384    q = repo.mq
3385    applied = {p.name for p in q.applied}
3386    patch = None
3387    args = list(args)
3388    if opts.get('list'):
3389        if args or opts.get('none'):
3390            raise error.Abort(
3391                _(b'cannot mix -l/--list with options or arguments')
3392            )
3393        for i in pycompat.xrange(len(q.series)):
3394            status(i)
3395        return
3396    if not args or args[0][0:1] in b'-+':
3397        if not q.applied:
3398            raise error.Abort(_(b'no patches applied'))
3399        patch = q.applied[-1].name
3400    if patch is None and args[0][0:1] not in b'-+':
3401        patch = args.pop(0)
3402    if patch is None:
3403        raise error.Abort(_(b'no patch to work with'))
3404    if args or opts.get('none'):
3405        idx = q.findseries(patch)
3406        if idx is None:
3407            raise error.Abort(_(b'no patch named %s') % patch)
3408        q.setguards(idx, args)
3409        q.savedirty()
3410    else:
3411        status(q.series.index(q.lookup(patch)))
3412
3413
3414@command(
3415    b"qheader",
3416    [],
3417    _(b'hg qheader [PATCH]'),
3418    helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
3419)
3420def header(ui, repo, patch=None):
3421    """print the header of the topmost or specified patch
3422
3423    Returns 0 on success."""
3424    q = repo.mq
3425
3426    if patch:
3427        patch = q.lookup(patch)
3428    else:
3429        if not q.applied:
3430            ui.write(_(b'no patches applied\n'))
3431            return 1
3432        patch = q.lookup(b'qtip')
3433    ph = patchheader(q.join(patch), q.plainmode)
3434
3435    ui.write(b'\n'.join(ph.message) + b'\n')
3436
3437
3438def lastsavename(path):
3439    (directory, base) = os.path.split(path)
3440    names = os.listdir(directory)
3441    namere = re.compile(b"%s.([0-9]+)" % base)
3442    maxindex = None
3443    maxname = None
3444    for f in names:
3445        m = namere.match(f)
3446        if m:
3447            index = int(m.group(1))
3448            if maxindex is None or index > maxindex:
3449                maxindex = index
3450                maxname = f
3451    if maxname:
3452        return (os.path.join(directory, maxname), maxindex)
3453    return (None, None)
3454
3455
3456def savename(path):
3457    (last, index) = lastsavename(path)
3458    if last is None:
3459        index = 0
3460    newpath = path + b".%d" % (index + 1)
3461    return newpath
3462
3463
3464@command(
3465    b"qpush",
3466    [
3467        (
3468            b'',
3469            b'keep-changes',
3470            None,
3471            _(b'tolerate non-conflicting local changes'),
3472        ),
3473        (b'f', b'force', None, _(b'apply on top of local changes')),
3474        (
3475            b'e',
3476            b'exact',
3477            None,
3478            _(b'apply the target patch to its recorded parent'),
3479        ),
3480        (b'l', b'list', None, _(b'list patch name in commit text')),
3481        (b'a', b'all', None, _(b'apply all patches')),
3482        (b'm', b'merge', None, _(b'merge from another queue (DEPRECATED)')),
3483        (b'n', b'name', b'', _(b'merge queue name (DEPRECATED)'), _(b'NAME')),
3484        (
3485            b'',
3486            b'move',
3487            None,
3488            _(b'reorder patch series and apply only the patch'),
3489        ),
3490        (b'', b'no-backup', None, _(b'do not save backup copies of files')),
3491    ],
3492    _(b'hg qpush [-f] [-l] [-a] [--move] [PATCH | INDEX]'),
3493    helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
3494    helpbasic=True,
3495)
3496def push(ui, repo, patch=None, **opts):
3497    """push the next patch onto the stack
3498
3499    By default, abort if the working directory contains uncommitted
3500    changes. With --keep-changes, abort only if the uncommitted files
3501    overlap with patched files. With -f/--force, backup and patch over
3502    uncommitted changes.
3503
3504    Return 0 on success.
3505    """
3506    q = repo.mq
3507    mergeq = None
3508
3509    opts = pycompat.byteskwargs(opts)
3510    opts = fixkeepchangesopts(ui, opts)
3511    if opts.get(b'merge'):
3512        if opts.get(b'name'):
3513            newpath = repo.vfs.join(opts.get(b'name'))
3514        else:
3515            newpath, i = lastsavename(q.path)
3516        if not newpath:
3517            ui.warn(_(b"no saved queues found, please use -n\n"))
3518            return 1
3519        mergeq = queue(ui, repo.baseui, repo.path, newpath)
3520        ui.warn(_(b"merging with queue at: %s\n") % mergeq.path)
3521    ret = q.push(
3522        repo,
3523        patch,
3524        force=opts.get(b'force'),
3525        list=opts.get(b'list'),
3526        mergeq=mergeq,
3527        all=opts.get(b'all'),
3528        move=opts.get(b'move'),
3529        exact=opts.get(b'exact'),
3530        nobackup=opts.get(b'no_backup'),
3531        keepchanges=opts.get(b'keep_changes'),
3532    )
3533    return ret
3534
3535
3536@command(
3537    b"qpop",
3538    [
3539        (b'a', b'all', None, _(b'pop all patches')),
3540        (b'n', b'name', b'', _(b'queue name to pop (DEPRECATED)'), _(b'NAME')),
3541        (
3542            b'',
3543            b'keep-changes',
3544            None,
3545            _(b'tolerate non-conflicting local changes'),
3546        ),
3547        (b'f', b'force', None, _(b'forget any local changes to patched files')),
3548        (b'', b'no-backup', None, _(b'do not save backup copies of files')),
3549    ],
3550    _(b'hg qpop [-a] [-f] [PATCH | INDEX]'),
3551    helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
3552    helpbasic=True,
3553)
3554def pop(ui, repo, patch=None, **opts):
3555    """pop the current patch off the stack
3556
3557    Without argument, pops off the top of the patch stack. If given a
3558    patch name, keeps popping off patches until the named patch is at
3559    the top of the stack.
3560
3561    By default, abort if the working directory contains uncommitted
3562    changes. With --keep-changes, abort only if the uncommitted files
3563    overlap with patched files. With -f/--force, backup and discard
3564    changes made to such files.
3565
3566    Return 0 on success.
3567    """
3568    opts = pycompat.byteskwargs(opts)
3569    opts = fixkeepchangesopts(ui, opts)
3570    localupdate = True
3571    if opts.get(b'name'):
3572        q = queue(ui, repo.baseui, repo.path, repo.vfs.join(opts.get(b'name')))
3573        ui.warn(_(b'using patch queue: %s\n') % q.path)
3574        localupdate = False
3575    else:
3576        q = repo.mq
3577    ret = q.pop(
3578        repo,
3579        patch,
3580        force=opts.get(b'force'),
3581        update=localupdate,
3582        all=opts.get(b'all'),
3583        nobackup=opts.get(b'no_backup'),
3584        keepchanges=opts.get(b'keep_changes'),
3585    )
3586    q.savedirty()
3587    return ret
3588
3589
3590@command(
3591    b"qrename|qmv",
3592    [],
3593    _(b'hg qrename PATCH1 [PATCH2]'),
3594    helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
3595)
3596def rename(ui, repo, patch, name=None, **opts):
3597    """rename a patch
3598
3599    With one argument, renames the current patch to PATCH1.
3600    With two arguments, renames PATCH1 to PATCH2.
3601
3602    Returns 0 on success."""
3603    q = repo.mq
3604    if not name:
3605        name = patch
3606        patch = None
3607
3608    if patch:
3609        patch = q.lookup(patch)
3610    else:
3611        if not q.applied:
3612            ui.write(_(b'no patches applied\n'))
3613            return
3614        patch = q.lookup(b'qtip')
3615    absdest = q.join(name)
3616    if os.path.isdir(absdest):
3617        name = normname(os.path.join(name, os.path.basename(patch)))
3618        absdest = q.join(name)
3619    q.checkpatchname(name)
3620
3621    ui.note(_(b'renaming %s to %s\n') % (patch, name))
3622    i = q.findseries(patch)
3623    guards = q.guard_re.findall(q.fullseries[i])
3624    q.fullseries[i] = name + b''.join([b' #' + g for g in guards])
3625    q.parseseries()
3626    q.seriesdirty = True
3627
3628    info = q.isapplied(patch)
3629    if info:
3630        q.applied[info[0]] = statusentry(info[1], name)
3631    q.applieddirty = True
3632
3633    destdir = os.path.dirname(absdest)
3634    if not os.path.isdir(destdir):
3635        os.makedirs(destdir)
3636    util.rename(q.join(patch), absdest)
3637    r = q.qrepo()
3638    if r and patch in r.dirstate:
3639        wctx = r[None]
3640        with r.wlock():
3641            if r.dirstate.get_entry(patch).added:
3642                r.dirstate.set_untracked(patch)
3643                r.dirstate.set_tracked(name)
3644            else:
3645                wctx.copy(patch, name)
3646                wctx.forget([patch])
3647
3648    q.savedirty()
3649
3650
3651@command(
3652    b"qrestore",
3653    [
3654        (b'd', b'delete', None, _(b'delete save entry')),
3655        (b'u', b'update', None, _(b'update queue working directory')),
3656    ],
3657    _(b'hg qrestore [-d] [-u] REV'),
3658    helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
3659)
3660def restore(ui, repo, rev, **opts):
3661    """restore the queue state saved by a revision (DEPRECATED)
3662
3663    This command is deprecated, use :hg:`rebase` instead."""
3664    rev = repo.lookup(rev)
3665    q = repo.mq
3666    q.restore(repo, rev, delete=opts.get('delete'), qupdate=opts.get('update'))
3667    q.savedirty()
3668    return 0
3669
3670
3671@command(
3672    b"qsave",
3673    [
3674        (b'c', b'copy', None, _(b'copy patch directory')),
3675        (b'n', b'name', b'', _(b'copy directory name'), _(b'NAME')),
3676        (b'e', b'empty', None, _(b'clear queue status file')),
3677        (b'f', b'force', None, _(b'force copy')),
3678    ]
3679    + cmdutil.commitopts,
3680    _(b'hg qsave [-m TEXT] [-l FILE] [-c] [-n NAME] [-e] [-f]'),
3681    helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
3682)
3683def save(ui, repo, **opts):
3684    """save current queue state (DEPRECATED)
3685
3686    This command is deprecated, use :hg:`rebase` instead."""
3687    q = repo.mq
3688    opts = pycompat.byteskwargs(opts)
3689    message = cmdutil.logmessage(ui, opts)
3690    ret = q.save(repo, msg=message)
3691    if ret:
3692        return ret
3693    q.savedirty()  # save to .hg/patches before copying
3694    if opts.get(b'copy'):
3695        path = q.path
3696        if opts.get(b'name'):
3697            newpath = os.path.join(q.basepath, opts.get(b'name'))
3698            if os.path.exists(newpath):
3699                if not os.path.isdir(newpath):
3700                    raise error.Abort(
3701                        _(b'destination %s exists and is not a directory')
3702                        % newpath
3703                    )
3704                if not opts.get(b'force'):
3705                    raise error.Abort(
3706                        _(b'destination %s exists, use -f to force') % newpath
3707                    )
3708        else:
3709            newpath = savename(path)
3710        ui.warn(_(b"copy %s to %s\n") % (path, newpath))
3711        util.copyfiles(path, newpath)
3712    if opts.get(b'empty'):
3713        del q.applied[:]
3714        q.applieddirty = True
3715        q.savedirty()
3716    return 0
3717
3718
3719@command(
3720    b"qselect",
3721    [
3722        (b'n', b'none', None, _(b'disable all guards')),
3723        (b's', b'series', None, _(b'list all guards in series file')),
3724        (b'', b'pop', None, _(b'pop to before first guarded applied patch')),
3725        (b'', b'reapply', None, _(b'pop, then reapply patches')),
3726    ],
3727    _(b'hg qselect [OPTION]... [GUARD]...'),
3728    helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
3729)
3730def select(ui, repo, *args, **opts):
3731    """set or print guarded patches to push
3732
3733    Use the :hg:`qguard` command to set or print guards on patch, then use
3734    qselect to tell mq which guards to use. A patch will be pushed if
3735    it has no guards or any positive guards match the currently
3736    selected guard, but will not be pushed if any negative guards
3737    match the current guard. For example::
3738
3739        qguard foo.patch -- -stable    (negative guard)
3740        qguard bar.patch    +stable    (positive guard)
3741        qselect stable
3742
3743    This activates the "stable" guard. mq will skip foo.patch (because
3744    it has a negative match) but push bar.patch (because it has a
3745    positive match).
3746
3747    With no arguments, prints the currently active guards.
3748    With one argument, sets the active guard.
3749
3750    Use -n/--none to deactivate guards (no other arguments needed).
3751    When no guards are active, patches with positive guards are
3752    skipped and patches with negative guards are pushed.
3753
3754    qselect can change the guards on applied patches. It does not pop
3755    guarded patches by default. Use --pop to pop back to the last
3756    applied patch that is not guarded. Use --reapply (which implies
3757    --pop) to push back to the current patch afterwards, but skip
3758    guarded patches.
3759
3760    Use -s/--series to print a list of all guards in the series file
3761    (no other arguments needed). Use -v for more information.
3762
3763    Returns 0 on success."""
3764
3765    q = repo.mq
3766    opts = pycompat.byteskwargs(opts)
3767    guards = q.active()
3768    pushable = lambda i: q.pushable(q.applied[i].name)[0]
3769    if args or opts.get(b'none'):
3770        old_unapplied = q.unapplied(repo)
3771        old_guarded = [
3772            i for i in pycompat.xrange(len(q.applied)) if not pushable(i)
3773        ]
3774        q.setactive(args)
3775        q.savedirty()
3776        if not args:
3777            ui.status(_(b'guards deactivated\n'))
3778        if not opts.get(b'pop') and not opts.get(b'reapply'):
3779            unapplied = q.unapplied(repo)
3780            guarded = [
3781                i for i in pycompat.xrange(len(q.applied)) if not pushable(i)
3782            ]
3783            if len(unapplied) != len(old_unapplied):
3784                ui.status(
3785                    _(
3786                        b'number of unguarded, unapplied patches has '
3787                        b'changed from %d to %d\n'
3788                    )
3789                    % (len(old_unapplied), len(unapplied))
3790                )
3791            if len(guarded) != len(old_guarded):
3792                ui.status(
3793                    _(
3794                        b'number of guarded, applied patches has changed '
3795                        b'from %d to %d\n'
3796                    )
3797                    % (len(old_guarded), len(guarded))
3798                )
3799    elif opts.get(b'series'):
3800        guards = {}
3801        noguards = 0
3802        for gs in q.seriesguards:
3803            if not gs:
3804                noguards += 1
3805            for g in gs:
3806                guards.setdefault(g, 0)
3807                guards[g] += 1
3808        if ui.verbose:
3809            guards[b'NONE'] = noguards
3810        guards = list(guards.items())
3811        guards.sort(key=lambda x: x[0][1:])
3812        if guards:
3813            ui.note(_(b'guards in series file:\n'))
3814            for guard, count in guards:
3815                ui.note(b'%2d  ' % count)
3816                ui.write(guard, b'\n')
3817        else:
3818            ui.note(_(b'no guards in series file\n'))
3819    else:
3820        if guards:
3821            ui.note(_(b'active guards:\n'))
3822            for g in guards:
3823                ui.write(g, b'\n')
3824        else:
3825            ui.write(_(b'no active guards\n'))
3826    reapply = opts.get(b'reapply') and q.applied and q.applied[-1].name
3827    popped = False
3828    if opts.get(b'pop') or opts.get(b'reapply'):
3829        for i in pycompat.xrange(len(q.applied)):
3830            if not pushable(i):
3831                ui.status(_(b'popping guarded patches\n'))
3832                popped = True
3833                if i == 0:
3834                    q.pop(repo, all=True)
3835                else:
3836                    q.pop(repo, q.applied[i - 1].name)
3837                break
3838    if popped:
3839        try:
3840            if reapply:
3841                ui.status(_(b'reapplying unguarded patches\n'))
3842                q.push(repo, reapply)
3843        finally:
3844            q.savedirty()
3845
3846
3847@command(
3848    b"qfinish",
3849    [(b'a', b'applied', None, _(b'finish all applied changesets'))],
3850    _(b'hg qfinish [-a] [REV]...'),
3851    helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
3852)
3853def finish(ui, repo, *revrange, **opts):
3854    """move applied patches into repository history
3855
3856    Finishes the specified revisions (corresponding to applied
3857    patches) by moving them out of mq control into regular repository
3858    history.
3859
3860    Accepts a revision range or the -a/--applied option. If --applied
3861    is specified, all applied mq revisions are removed from mq
3862    control. Otherwise, the given revisions must be at the base of the
3863    stack of applied patches.
3864
3865    This can be especially useful if your changes have been applied to
3866    an upstream repository, or if you are about to push your changes
3867    to upstream.
3868
3869    Returns 0 on success.
3870    """
3871    if not opts.get('applied') and not revrange:
3872        raise error.Abort(_(b'no revisions specified'))
3873    elif opts.get('applied'):
3874        revrange = (b'qbase::qtip',) + revrange
3875
3876    q = repo.mq
3877    if not q.applied:
3878        ui.status(_(b'no patches applied\n'))
3879        return 0
3880
3881    revs = logcmdutil.revrange(repo, revrange)
3882    if repo[b'.'].rev() in revs and repo[None].files():
3883        ui.warn(_(b'warning: uncommitted changes in the working directory\n'))
3884    # queue.finish may changes phases but leave the responsibility to lock the
3885    # repo to the caller to avoid deadlock with wlock. This command code is
3886    # responsibility for this locking.
3887    with repo.lock():
3888        q.finish(repo, revs)
3889        q.savedirty()
3890    return 0
3891
3892
3893@command(
3894    b"qqueue",
3895    [
3896        (b'l', b'list', False, _(b'list all available queues')),
3897        (b'', b'active', False, _(b'print name of active queue')),
3898        (b'c', b'create', False, _(b'create new queue')),
3899        (b'', b'rename', False, _(b'rename active queue')),
3900        (b'', b'delete', False, _(b'delete reference to queue')),
3901        (b'', b'purge', False, _(b'delete queue, and remove patch dir')),
3902    ],
3903    _(b'[OPTION] [QUEUE]'),
3904    helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
3905)
3906def qqueue(ui, repo, name=None, **opts):
3907    """manage multiple patch queues
3908
3909    Supports switching between different patch queues, as well as creating
3910    new patch queues and deleting existing ones.
3911
3912    Omitting a queue name or specifying -l/--list will show you the registered
3913    queues - by default the "normal" patches queue is registered. The currently
3914    active queue will be marked with "(active)". Specifying --active will print
3915    only the name of the active queue.
3916
3917    To create a new queue, use -c/--create. The queue is automatically made
3918    active, except in the case where there are applied patches from the
3919    currently active queue in the repository. Then the queue will only be
3920    created and switching will fail.
3921
3922    To delete an existing queue, use --delete. You cannot delete the currently
3923    active queue.
3924
3925    Returns 0 on success.
3926    """
3927    q = repo.mq
3928    _defaultqueue = b'patches'
3929    _allqueues = b'patches.queues'
3930    _activequeue = b'patches.queue'
3931
3932    def _getcurrent():
3933        cur = os.path.basename(q.path)
3934        if cur.startswith(b'patches-'):
3935            cur = cur[8:]
3936        return cur
3937
3938    def _noqueues():
3939        try:
3940            fh = repo.vfs(_allqueues, b'r')
3941            fh.close()
3942        except IOError:
3943            return True
3944
3945        return False
3946
3947    def _getqueues():
3948        current = _getcurrent()
3949
3950        try:
3951            fh = repo.vfs(_allqueues, b'r')
3952            queues = [queue.strip() for queue in fh if queue.strip()]
3953            fh.close()
3954            if current not in queues:
3955                queues.append(current)
3956        except IOError:
3957            queues = [_defaultqueue]
3958
3959        return sorted(queues)
3960
3961    def _setactive(name):
3962        if q.applied:
3963            raise error.Abort(
3964                _(
3965                    b'new queue created, but cannot make active '
3966                    b'as patches are applied'
3967                )
3968            )
3969        _setactivenocheck(name)
3970
3971    def _setactivenocheck(name):
3972        fh = repo.vfs(_activequeue, b'w')
3973        if name != b'patches':
3974            fh.write(name)
3975        fh.close()
3976
3977    def _addqueue(name):
3978        fh = repo.vfs(_allqueues, b'a')
3979        fh.write(b'%s\n' % (name,))
3980        fh.close()
3981
3982    def _queuedir(name):
3983        if name == b'patches':
3984            return repo.vfs.join(b'patches')
3985        else:
3986            return repo.vfs.join(b'patches-' + name)
3987
3988    def _validname(name):
3989        for n in name:
3990            if n in b':\\/.':
3991                return False
3992        return True
3993
3994    def _delete(name):
3995        if name not in existing:
3996            raise error.Abort(_(b'cannot delete queue that does not exist'))
3997
3998        current = _getcurrent()
3999
4000        if name == current:
4001            raise error.Abort(_(b'cannot delete currently active queue'))
4002
4003        fh = repo.vfs(b'patches.queues.new', b'w')
4004        for queue in existing:
4005            if queue == name:
4006                continue
4007            fh.write(b'%s\n' % (queue,))
4008        fh.close()
4009        repo.vfs.rename(b'patches.queues.new', _allqueues)
4010
4011    opts = pycompat.byteskwargs(opts)
4012    if not name or opts.get(b'list') or opts.get(b'active'):
4013        current = _getcurrent()
4014        if opts.get(b'active'):
4015            ui.write(b'%s\n' % (current,))
4016            return
4017        for queue in _getqueues():
4018            ui.write(b'%s' % (queue,))
4019            if queue == current and not ui.quiet:
4020                ui.write(_(b' (active)\n'))
4021            else:
4022                ui.write(b'\n')
4023        return
4024
4025    if not _validname(name):
4026        raise error.Abort(
4027            _(b'invalid queue name, may not contain the characters ":\\/."')
4028        )
4029
4030    with repo.wlock():
4031        existing = _getqueues()
4032
4033        if opts.get(b'create'):
4034            if name in existing:
4035                raise error.Abort(_(b'queue "%s" already exists') % name)
4036            if _noqueues():
4037                _addqueue(_defaultqueue)
4038            _addqueue(name)
4039            _setactive(name)
4040        elif opts.get(b'rename'):
4041            current = _getcurrent()
4042            if name == current:
4043                raise error.Abort(
4044                    _(b'can\'t rename "%s" to its current name') % name
4045                )
4046            if name in existing:
4047                raise error.Abort(_(b'queue "%s" already exists') % name)
4048
4049            olddir = _queuedir(current)
4050            newdir = _queuedir(name)
4051
4052            if os.path.exists(newdir):
4053                raise error.Abort(
4054                    _(b'non-queue directory "%s" already exists') % newdir
4055                )
4056
4057            fh = repo.vfs(b'patches.queues.new', b'w')
4058            for queue in existing:
4059                if queue == current:
4060                    fh.write(b'%s\n' % (name,))
4061                    if os.path.exists(olddir):
4062                        util.rename(olddir, newdir)
4063                else:
4064                    fh.write(b'%s\n' % (queue,))
4065            fh.close()
4066            repo.vfs.rename(b'patches.queues.new', _allqueues)
4067            _setactivenocheck(name)
4068        elif opts.get(b'delete'):
4069            _delete(name)
4070        elif opts.get(b'purge'):
4071            if name in existing:
4072                _delete(name)
4073            qdir = _queuedir(name)
4074            if os.path.exists(qdir):
4075                shutil.rmtree(qdir)
4076        else:
4077            if name not in existing:
4078                raise error.Abort(_(b'use --create to create a new queue'))
4079            _setactive(name)
4080
4081
4082def mqphasedefaults(repo, roots):
4083    """callback used to set mq changeset as secret when no phase data exists"""
4084    if repo.mq.applied:
4085        if repo.ui.configbool(b'mq', b'secret'):
4086            mqphase = phases.secret
4087        else:
4088            mqphase = phases.draft
4089        qbase = repo[repo.mq.applied[0].node]
4090        roots[mqphase].add(qbase.node())
4091    return roots
4092
4093
4094def reposetup(ui, repo):
4095    class mqrepo(repo.__class__):
4096        @localrepo.unfilteredpropertycache
4097        def mq(self):
4098            return queue(self.ui, self.baseui, self.path)
4099
4100        def invalidateall(self):
4101            super(mqrepo, self).invalidateall()
4102            if localrepo.hasunfilteredcache(self, 'mq'):
4103                # recreate mq in case queue path was changed
4104                delattr(self.unfiltered(), 'mq')
4105
4106        def abortifwdirpatched(self, errmsg, force=False):
4107            if self.mq.applied and self.mq.checkapplied and not force:
4108                parents = self.dirstate.parents()
4109                patches = [s.node for s in self.mq.applied]
4110                if any(p in patches for p in parents):
4111                    raise error.Abort(errmsg)
4112
4113        def commit(
4114            self,
4115            text=b"",
4116            user=None,
4117            date=None,
4118            match=None,
4119            force=False,
4120            editor=False,
4121            extra=None,
4122        ):
4123            if extra is None:
4124                extra = {}
4125            self.abortifwdirpatched(
4126                _(b'cannot commit over an applied mq patch'), force
4127            )
4128
4129            return super(mqrepo, self).commit(
4130                text, user, date, match, force, editor, extra
4131            )
4132
4133        def checkpush(self, pushop):
4134            if self.mq.applied and self.mq.checkapplied and not pushop.force:
4135                outapplied = [e.node for e in self.mq.applied]
4136                if pushop.revs:
4137                    # Assume applied patches have no non-patch descendants and
4138                    # are not on remote already. Filtering any changeset not
4139                    # pushed.
4140                    heads = set(pushop.revs)
4141                    for node in reversed(outapplied):
4142                        if node in heads:
4143                            break
4144                        else:
4145                            outapplied.pop()
4146                # looking for pushed and shared changeset
4147                for node in outapplied:
4148                    if self[node].phase() < phases.secret:
4149                        raise error.Abort(_(b'source has mq patches applied'))
4150                # no non-secret patches pushed
4151            super(mqrepo, self).checkpush(pushop)
4152
4153        def _findtags(self):
4154            '''augment tags from base class with patch tags'''
4155            result = super(mqrepo, self)._findtags()
4156
4157            q = self.mq
4158            if not q.applied:
4159                return result
4160
4161            mqtags = [(patch.node, patch.name) for patch in q.applied]
4162
4163            try:
4164                # for now ignore filtering business
4165                self.unfiltered().changelog.rev(mqtags[-1][0])
4166            except error.LookupError:
4167                self.ui.warn(
4168                    _(b'mq status file refers to unknown node %s\n')
4169                    % short(mqtags[-1][0])
4170                )
4171                return result
4172
4173            # do not add fake tags for filtered revisions
4174            included = self.changelog.hasnode
4175            mqtags = [mqt for mqt in mqtags if included(mqt[0])]
4176            if not mqtags:
4177                return result
4178
4179            mqtags.append((mqtags[-1][0], b'qtip'))
4180            mqtags.append((mqtags[0][0], b'qbase'))
4181            mqtags.append((self.changelog.parents(mqtags[0][0])[0], b'qparent'))
4182            tags = result[0]
4183            for patch in mqtags:
4184                if patch[1] in tags:
4185                    self.ui.warn(
4186                        _(b'tag %s overrides mq patch of the same name\n')
4187                        % patch[1]
4188                    )
4189                else:
4190                    tags[patch[1]] = patch[0]
4191
4192            return result
4193
4194    if repo.local():
4195        repo.__class__ = mqrepo
4196
4197        repo._phasedefaults.append(mqphasedefaults)
4198
4199
4200def mqimport(orig, ui, repo, *args, **kwargs):
4201    if util.safehasattr(repo, b'abortifwdirpatched') and not kwargs.get(
4202        'no_commit', False
4203    ):
4204        repo.abortifwdirpatched(
4205            _(b'cannot import over an applied patch'), kwargs.get('force')
4206        )
4207    return orig(ui, repo, *args, **kwargs)
4208
4209
4210def mqinit(orig, ui, *args, **kwargs):
4211    mq = kwargs.pop('mq', None)
4212
4213    if not mq:
4214        return orig(ui, *args, **kwargs)
4215
4216    if args:
4217        repopath = args[0]
4218        if not hg.islocal(repopath):
4219            raise error.Abort(
4220                _(b'only a local queue repository may be initialized')
4221            )
4222    else:
4223        repopath = cmdutil.findrepo(encoding.getcwd())
4224        if not repopath:
4225            raise error.Abort(
4226                _(b'there is no Mercurial repository here (.hg not found)')
4227            )
4228    repo = hg.repository(ui, repopath)
4229    return qinit(ui, repo, True)
4230
4231
4232def mqcommand(orig, ui, repo, *args, **kwargs):
4233    """Add --mq option to operate on patch repository instead of main"""
4234
4235    # some commands do not like getting unknown options
4236    mq = kwargs.pop('mq', None)
4237
4238    if not mq:
4239        return orig(ui, repo, *args, **kwargs)
4240
4241    q = repo.mq
4242    r = q.qrepo()
4243    if not r:
4244        raise error.Abort(_(b'no queue repository'))
4245    return orig(r.ui, r, *args, **kwargs)
4246
4247
4248def summaryhook(ui, repo):
4249    q = repo.mq
4250    m = []
4251    a, u = len(q.applied), len(q.unapplied(repo))
4252    if a:
4253        m.append(ui.label(_(b"%d applied"), b'qseries.applied') % a)
4254    if u:
4255        m.append(ui.label(_(b"%d unapplied"), b'qseries.unapplied') % u)
4256    if m:
4257        # i18n: column positioning for "hg summary"
4258        ui.write(_(b"mq:     %s\n") % b', '.join(m))
4259    else:
4260        # i18n: column positioning for "hg summary"
4261        ui.note(_(b"mq:     (empty queue)\n"))
4262
4263
4264revsetpredicate = registrar.revsetpredicate()
4265
4266
4267@revsetpredicate(b'mq()')
4268def revsetmq(repo, subset, x):
4269    """Changesets managed by MQ."""
4270    revsetlang.getargs(x, 0, 0, _(b"mq takes no arguments"))
4271    applied = {repo[r.node].rev() for r in repo.mq.applied}
4272    return smartset.baseset([r for r in subset if r in applied])
4273
4274
4275# tell hggettext to extract docstrings from these functions:
4276i18nfunctions = [revsetmq]
4277
4278
4279def extsetup(ui):
4280    # Ensure mq wrappers are called first, regardless of extension load order by
4281    # NOT wrapping in uisetup() and instead deferring to init stage two here.
4282    mqopt = [(b'', b'mq', None, _(b"operate on patch repository"))]
4283
4284    extensions.wrapcommand(commands.table, b'import', mqimport)
4285    cmdutil.summaryhooks.add(b'mq', summaryhook)
4286
4287    entry = extensions.wrapcommand(commands.table, b'init', mqinit)
4288    entry[1].extend(mqopt)
4289
4290    def dotable(cmdtable):
4291        for cmd, entry in pycompat.iteritems(cmdtable):
4292            cmd = cmdutil.parsealiases(cmd)[0]
4293            func = entry[0]
4294            if func.norepo:
4295                continue
4296            entry = extensions.wrapcommand(cmdtable, cmd, mqcommand)
4297            entry[1].extend(mqopt)
4298
4299    dotable(commands.table)
4300
4301    thismodule = sys.modules["hgext.mq"]
4302    for extname, extmodule in extensions.extensions():
4303        if extmodule != thismodule:
4304            dotable(getattr(extmodule, 'cmdtable', {}))
4305
4306
4307colortable = {
4308    b'qguard.negative': b'red',
4309    b'qguard.positive': b'yellow',
4310    b'qguard.unguarded': b'green',
4311    b'qseries.applied': b'blue bold underline',
4312    b'qseries.guarded': b'black bold',
4313    b'qseries.missing': b'red bold',
4314    b'qseries.unapplied': b'black bold',
4315}
4316