hgext/transplant.py
changeset 43077 687b865b95ad
parent 43076 2372284d9457
child 43085 eef9a2d67051
equal deleted inserted replaced
43076:2372284d9457 43077:687b865b95ad
    53 command = registrar.command(cmdtable)
    53 command = registrar.command(cmdtable)
    54 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
    54 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
    55 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
    55 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
    56 # be specifying the version(s) of Mercurial they are tested with, or
    56 # be specifying the version(s) of Mercurial they are tested with, or
    57 # leave the attribute unspecified.
    57 # leave the attribute unspecified.
    58 testedwith = 'ships-with-hg-core'
    58 testedwith = b'ships-with-hg-core'
    59 
    59 
    60 configtable = {}
    60 configtable = {}
    61 configitem = registrar.configitem(configtable)
    61 configitem = registrar.configitem(configtable)
    62 
    62 
    63 configitem(
    63 configitem(
    64     'transplant', 'filter', default=None,
    64     b'transplant', b'filter', default=None,
    65 )
    65 )
    66 configitem(
    66 configitem(
    67     'transplant', 'log', default=None,
    67     b'transplant', b'log', default=None,
    68 )
    68 )
    69 
    69 
    70 
    70 
    71 class transplantentry(object):
    71 class transplantentry(object):
    72     def __init__(self, lnode, rnode):
    72     def __init__(self, lnode, rnode):
    88 
    88 
    89     def read(self):
    89     def read(self):
    90         abspath = os.path.join(self.path, self.transplantfile)
    90         abspath = os.path.join(self.path, self.transplantfile)
    91         if self.transplantfile and os.path.exists(abspath):
    91         if self.transplantfile and os.path.exists(abspath):
    92             for line in self.opener.read(self.transplantfile).splitlines():
    92             for line in self.opener.read(self.transplantfile).splitlines():
    93                 lnode, rnode = map(revlog.bin, line.split(':'))
    93                 lnode, rnode = map(revlog.bin, line.split(b':'))
    94                 list = self.transplants.setdefault(rnode, [])
    94                 list = self.transplants.setdefault(rnode, [])
    95                 list.append(transplantentry(lnode, rnode))
    95                 list.append(transplantentry(lnode, rnode))
    96 
    96 
    97     def write(self):
    97     def write(self):
    98         if self.dirty and self.transplantfile:
    98         if self.dirty and self.transplantfile:
    99             if not os.path.isdir(self.path):
    99             if not os.path.isdir(self.path):
   100                 os.mkdir(self.path)
   100                 os.mkdir(self.path)
   101             fp = self.opener(self.transplantfile, 'w')
   101             fp = self.opener(self.transplantfile, b'w')
   102             for list in self.transplants.itervalues():
   102             for list in self.transplants.itervalues():
   103                 for t in list:
   103                 for t in list:
   104                     l, r = map(nodemod.hex, (t.lnode, t.rnode))
   104                     l, r = map(nodemod.hex, (t.lnode, t.rnode))
   105                     fp.write(l + ':' + r + '\n')
   105                     fp.write(l + b':' + r + b'\n')
   106             fp.close()
   106             fp.close()
   107         self.dirty = False
   107         self.dirty = False
   108 
   108 
   109     def get(self, rnode):
   109     def get(self, rnode):
   110         return self.transplants.get(rnode) or []
   110         return self.transplants.get(rnode) or []
   122 
   122 
   123 
   123 
   124 class transplanter(object):
   124 class transplanter(object):
   125     def __init__(self, ui, repo, opts):
   125     def __init__(self, ui, repo, opts):
   126         self.ui = ui
   126         self.ui = ui
   127         self.path = repo.vfs.join('transplant')
   127         self.path = repo.vfs.join(b'transplant')
   128         self.opener = vfsmod.vfs(self.path)
   128         self.opener = vfsmod.vfs(self.path)
   129         self.transplants = transplants(
   129         self.transplants = transplants(
   130             self.path, 'transplants', opener=self.opener
   130             self.path, b'transplants', opener=self.opener
   131         )
   131         )
   132 
   132 
   133         def getcommiteditor():
   133         def getcommiteditor():
   134             editform = cmdutil.mergeeditform(repo[None], 'transplant')
   134             editform = cmdutil.mergeeditform(repo[None], b'transplant')
   135             return cmdutil.getcommiteditor(
   135             return cmdutil.getcommiteditor(
   136                 editform=editform, **pycompat.strkwargs(opts)
   136                 editform=editform, **pycompat.strkwargs(opts)
   137             )
   137             )
   138 
   138 
   139         self.getcommiteditor = getcommiteditor
   139         self.getcommiteditor = getcommiteditor
   173         diffopts.git = True
   173         diffopts.git = True
   174 
   174 
   175         lock = tr = None
   175         lock = tr = None
   176         try:
   176         try:
   177             lock = repo.lock()
   177             lock = repo.lock()
   178             tr = repo.transaction('transplant')
   178             tr = repo.transaction(b'transplant')
   179             for rev in revs:
   179             for rev in revs:
   180                 node = revmap[rev]
   180                 node = revmap[rev]
   181                 revstr = '%d:%s' % (rev, nodemod.short(node))
   181                 revstr = b'%d:%s' % (rev, nodemod.short(node))
   182 
   182 
   183                 if self.applied(repo, node, p1):
   183                 if self.applied(repo, node, p1):
   184                     self.ui.warn(
   184                     self.ui.warn(
   185                         _('skipping already applied revision %s\n') % revstr
   185                         _(b'skipping already applied revision %s\n') % revstr
   186                     )
   186                     )
   187                     continue
   187                     continue
   188 
   188 
   189                 parents = source.changelog.parents(node)
   189                 parents = source.changelog.parents(node)
   190                 if not (opts.get('filter') or opts.get('log')):
   190                 if not (opts.get(b'filter') or opts.get(b'log')):
   191                     # If the changeset parent is the same as the
   191                     # If the changeset parent is the same as the
   192                     # wdir's parent, just pull it.
   192                     # wdir's parent, just pull it.
   193                     if parents[0] == p1:
   193                     if parents[0] == p1:
   194                         pulls.append(node)
   194                         pulls.append(node)
   195                         p1 = node
   195                         p1 = node
   212                     if not hasnode(repo, node):
   212                     if not hasnode(repo, node):
   213                         exchange.pull(repo, source.peer(), heads=[node])
   213                         exchange.pull(repo, source.peer(), heads=[node])
   214 
   214 
   215                 skipmerge = False
   215                 skipmerge = False
   216                 if parents[1] != revlog.nullid:
   216                 if parents[1] != revlog.nullid:
   217                     if not opts.get('parent'):
   217                     if not opts.get(b'parent'):
   218                         self.ui.note(
   218                         self.ui.note(
   219                             _('skipping merge changeset %d:%s\n')
   219                             _(b'skipping merge changeset %d:%s\n')
   220                             % (rev, nodemod.short(node))
   220                             % (rev, nodemod.short(node))
   221                         )
   221                         )
   222                         skipmerge = True
   222                         skipmerge = True
   223                     else:
   223                     else:
   224                         parent = source.lookup(opts['parent'])
   224                         parent = source.lookup(opts[b'parent'])
   225                         if parent not in parents:
   225                         if parent not in parents:
   226                             raise error.Abort(
   226                             raise error.Abort(
   227                                 _('%s is not a parent of %s')
   227                                 _(b'%s is not a parent of %s')
   228                                 % (nodemod.short(parent), nodemod.short(node))
   228                                 % (nodemod.short(parent), nodemod.short(node))
   229                             )
   229                             )
   230                 else:
   230                 else:
   231                     parent = parents[0]
   231                     parent = parents[0]
   232 
   232 
   233                 if skipmerge:
   233                 if skipmerge:
   234                     patchfile = None
   234                     patchfile = None
   235                 else:
   235                 else:
   236                     fd, patchfile = pycompat.mkstemp(prefix='hg-transplant-')
   236                     fd, patchfile = pycompat.mkstemp(prefix=b'hg-transplant-')
   237                     fp = os.fdopen(fd, r'wb')
   237                     fp = os.fdopen(fd, r'wb')
   238                     gen = patch.diff(source, parent, node, opts=diffopts)
   238                     gen = patch.diff(source, parent, node, opts=diffopts)
   239                     for chunk in gen:
   239                     for chunk in gen:
   240                         fp.write(chunk)
   240                         fp.write(chunk)
   241                     fp.close()
   241                     fp.close()
   248                                 repo,
   248                                 repo,
   249                                 node,
   249                                 node,
   250                                 source.changelog.read(node),
   250                                 source.changelog.read(node),
   251                                 patchfile,
   251                                 patchfile,
   252                                 merge=domerge,
   252                                 merge=domerge,
   253                                 log=opts.get('log'),
   253                                 log=opts.get(b'log'),
   254                                 filter=opts.get('filter'),
   254                                 filter=opts.get(b'filter'),
   255                             )
   255                             )
   256                         except TransplantError:
   256                         except TransplantError:
   257                             # Do not rollback, it is up to the user to
   257                             # Do not rollback, it is up to the user to
   258                             # fix the merge or cancel everything
   258                             # fix the merge or cancel everything
   259                             tr.close()
   259                             tr.close()
   260                             raise
   260                             raise
   261                         if n and domerge:
   261                         if n and domerge:
   262                             self.ui.status(
   262                             self.ui.status(
   263                                 _('%s merged at %s\n')
   263                                 _(b'%s merged at %s\n')
   264                                 % (revstr, nodemod.short(n))
   264                                 % (revstr, nodemod.short(n))
   265                             )
   265                             )
   266                         elif n:
   266                         elif n:
   267                             self.ui.status(
   267                             self.ui.status(
   268                                 _('%s transplanted to %s\n')
   268                                 _(b'%s transplanted to %s\n')
   269                                 % (nodemod.short(node), nodemod.short(n))
   269                                 % (nodemod.short(node), nodemod.short(n))
   270                             )
   270                             )
   271                     finally:
   271                     finally:
   272                         if patchfile:
   272                         if patchfile:
   273                             os.unlink(patchfile)
   273                             os.unlink(patchfile)
   284                 lock.release()
   284                 lock.release()
   285 
   285 
   286     def filter(self, filter, node, changelog, patchfile):
   286     def filter(self, filter, node, changelog, patchfile):
   287         '''arbitrarily rewrite changeset before applying it'''
   287         '''arbitrarily rewrite changeset before applying it'''
   288 
   288 
   289         self.ui.status(_('filtering %s\n') % patchfile)
   289         self.ui.status(_(b'filtering %s\n') % patchfile)
   290         user, date, msg = (changelog[1], changelog[2], changelog[4])
   290         user, date, msg = (changelog[1], changelog[2], changelog[4])
   291         fd, headerfile = pycompat.mkstemp(prefix='hg-transplant-')
   291         fd, headerfile = pycompat.mkstemp(prefix=b'hg-transplant-')
   292         fp = os.fdopen(fd, r'wb')
   292         fp = os.fdopen(fd, r'wb')
   293         fp.write("# HG changeset patch\n")
   293         fp.write(b"# HG changeset patch\n")
   294         fp.write("# User %s\n" % user)
   294         fp.write(b"# User %s\n" % user)
   295         fp.write("# Date %d %d\n" % date)
   295         fp.write(b"# Date %d %d\n" % date)
   296         fp.write(msg + '\n')
   296         fp.write(msg + b'\n')
   297         fp.close()
   297         fp.close()
   298 
   298 
   299         try:
   299         try:
   300             self.ui.system(
   300             self.ui.system(
   301                 '%s %s %s'
   301                 b'%s %s %s'
   302                 % (
   302                 % (
   303                     filter,
   303                     filter,
   304                     procutil.shellquote(headerfile),
   304                     procutil.shellquote(headerfile),
   305                     procutil.shellquote(patchfile),
   305                     procutil.shellquote(patchfile),
   306                 ),
   306                 ),
   307                 environ={
   307                 environ={
   308                     'HGUSER': changelog[1],
   308                     b'HGUSER': changelog[1],
   309                     'HGREVISION': nodemod.hex(node),
   309                     b'HGREVISION': nodemod.hex(node),
   310                 },
   310                 },
   311                 onerr=error.Abort,
   311                 onerr=error.Abort,
   312                 errprefix=_('filter failed'),
   312                 errprefix=_(b'filter failed'),
   313                 blockedtag='transplant_filter',
   313                 blockedtag=b'transplant_filter',
   314             )
   314             )
   315             user, date, msg = self.parselog(open(headerfile, 'rb'))[1:4]
   315             user, date, msg = self.parselog(open(headerfile, b'rb'))[1:4]
   316         finally:
   316         finally:
   317             os.unlink(headerfile)
   317             os.unlink(headerfile)
   318 
   318 
   319         return (user, date, msg)
   319         return (user, date, msg)
   320 
   320 
   321     def applyone(
   321     def applyone(
   322         self, repo, node, cl, patchfile, merge=False, log=False, filter=None
   322         self, repo, node, cl, patchfile, merge=False, log=False, filter=None
   323     ):
   323     ):
   324         '''apply the patch in patchfile to the repository as a transplant'''
   324         '''apply the patch in patchfile to the repository as a transplant'''
   325         (manifest, user, (time, timezone), files, message) = cl[:5]
   325         (manifest, user, (time, timezone), files, message) = cl[:5]
   326         date = "%d %d" % (time, timezone)
   326         date = b"%d %d" % (time, timezone)
   327         extra = {'transplant_source': node}
   327         extra = {b'transplant_source': node}
   328         if filter:
   328         if filter:
   329             (user, date, message) = self.filter(filter, node, cl, patchfile)
   329             (user, date, message) = self.filter(filter, node, cl, patchfile)
   330 
   330 
   331         if log:
   331         if log:
   332             # we don't translate messages inserted into commits
   332             # we don't translate messages inserted into commits
   333             message += '\n(transplanted from %s)' % nodemod.hex(node)
   333             message += b'\n(transplanted from %s)' % nodemod.hex(node)
   334 
   334 
   335         self.ui.status(_('applying %s\n') % nodemod.short(node))
   335         self.ui.status(_(b'applying %s\n') % nodemod.short(node))
   336         self.ui.note('%s %s\n%s\n' % (user, date, message))
   336         self.ui.note(b'%s %s\n%s\n' % (user, date, message))
   337 
   337 
   338         if not patchfile and not merge:
   338         if not patchfile and not merge:
   339             raise error.Abort(_('can only omit patchfile if merging'))
   339             raise error.Abort(_(b'can only omit patchfile if merging'))
   340         if patchfile:
   340         if patchfile:
   341             try:
   341             try:
   342                 files = set()
   342                 files = set()
   343                 patch.patch(self.ui, repo, patchfile, files=files, eolmode=None)
   343                 patch.patch(self.ui, repo, patchfile, files=files, eolmode=None)
   344                 files = list(files)
   344                 files = list(files)
   345             except Exception as inst:
   345             except Exception as inst:
   346                 seriespath = os.path.join(self.path, 'series')
   346                 seriespath = os.path.join(self.path, b'series')
   347                 if os.path.exists(seriespath):
   347                 if os.path.exists(seriespath):
   348                     os.unlink(seriespath)
   348                     os.unlink(seriespath)
   349                 p1 = repo.dirstate.p1()
   349                 p1 = repo.dirstate.p1()
   350                 p2 = node
   350                 p2 = node
   351                 self.log(user, date, message, p1, p2, merge=merge)
   351                 self.log(user, date, message, p1, p2, merge=merge)
   352                 self.ui.write(stringutil.forcebytestr(inst) + '\n')
   352                 self.ui.write(stringutil.forcebytestr(inst) + b'\n')
   353                 raise TransplantError(
   353                 raise TransplantError(
   354                     _(
   354                     _(
   355                         'fix up the working directory and run '
   355                         b'fix up the working directory and run '
   356                         'hg transplant --continue'
   356                         b'hg transplant --continue'
   357                     )
   357                     )
   358                 )
   358                 )
   359         else:
   359         else:
   360             files = None
   360             files = None
   361         if merge:
   361         if merge:
   373             match=m,
   373             match=m,
   374             editor=self.getcommiteditor(),
   374             editor=self.getcommiteditor(),
   375         )
   375         )
   376         if not n:
   376         if not n:
   377             self.ui.warn(
   377             self.ui.warn(
   378                 _('skipping emptied changeset %s\n') % nodemod.short(node)
   378                 _(b'skipping emptied changeset %s\n') % nodemod.short(node)
   379             )
   379             )
   380             return None
   380             return None
   381         if not merge:
   381         if not merge:
   382             self.transplants.set(n, node)
   382             self.transplants.set(n, node)
   383 
   383 
   384         return n
   384         return n
   385 
   385 
   386     def canresume(self):
   386     def canresume(self):
   387         return os.path.exists(os.path.join(self.path, 'journal'))
   387         return os.path.exists(os.path.join(self.path, b'journal'))
   388 
   388 
   389     def resume(self, repo, source, opts):
   389     def resume(self, repo, source, opts):
   390         '''recover last transaction and apply remaining changesets'''
   390         '''recover last transaction and apply remaining changesets'''
   391         if os.path.exists(os.path.join(self.path, 'journal')):
   391         if os.path.exists(os.path.join(self.path, b'journal')):
   392             n, node = self.recover(repo, source, opts)
   392             n, node = self.recover(repo, source, opts)
   393             if n:
   393             if n:
   394                 self.ui.status(
   394                 self.ui.status(
   395                     _('%s transplanted as %s\n')
   395                     _(b'%s transplanted as %s\n')
   396                     % (nodemod.short(node), nodemod.short(n))
   396                     % (nodemod.short(node), nodemod.short(n))
   397                 )
   397                 )
   398             else:
   398             else:
   399                 self.ui.status(
   399                 self.ui.status(
   400                     _('%s skipped due to empty diff\n') % (nodemod.short(node),)
   400                     _(b'%s skipped due to empty diff\n')
       
   401                     % (nodemod.short(node),)
   401                 )
   402                 )
   402         seriespath = os.path.join(self.path, 'series')
   403         seriespath = os.path.join(self.path, b'series')
   403         if not os.path.exists(seriespath):
   404         if not os.path.exists(seriespath):
   404             self.transplants.write()
   405             self.transplants.write()
   405             return
   406             return
   406         nodes, merges = self.readseries()
   407         nodes, merges = self.readseries()
   407         revmap = {}
   408         revmap = {}
   415         '''commit working directory using journal metadata'''
   416         '''commit working directory using journal metadata'''
   416         node, user, date, message, parents = self.readlog()
   417         node, user, date, message, parents = self.readlog()
   417         merge = False
   418         merge = False
   418 
   419 
   419         if not user or not date or not message or not parents[0]:
   420         if not user or not date or not message or not parents[0]:
   420             raise error.Abort(_('transplant log file is corrupt'))
   421             raise error.Abort(_(b'transplant log file is corrupt'))
   421 
   422 
   422         parent = parents[0]
   423         parent = parents[0]
   423         if len(parents) > 1:
   424         if len(parents) > 1:
   424             if opts.get('parent'):
   425             if opts.get(b'parent'):
   425                 parent = source.lookup(opts['parent'])
   426                 parent = source.lookup(opts[b'parent'])
   426                 if parent not in parents:
   427                 if parent not in parents:
   427                     raise error.Abort(
   428                     raise error.Abort(
   428                         _('%s is not a parent of %s')
   429                         _(b'%s is not a parent of %s')
   429                         % (nodemod.short(parent), nodemod.short(node))
   430                         % (nodemod.short(parent), nodemod.short(node))
   430                     )
   431                     )
   431             else:
   432             else:
   432                 merge = True
   433                 merge = True
   433 
   434 
   434         extra = {'transplant_source': node}
   435         extra = {b'transplant_source': node}
   435         try:
   436         try:
   436             p1 = repo.dirstate.p1()
   437             p1 = repo.dirstate.p1()
   437             if p1 != parent:
   438             if p1 != parent:
   438                 raise error.Abort(
   439                 raise error.Abort(
   439                     _('working directory not at transplant ' 'parent %s')
   440                     _(b'working directory not at transplant ' b'parent %s')
   440                     % nodemod.hex(parent)
   441                     % nodemod.hex(parent)
   441                 )
   442                 )
   442             if merge:
   443             if merge:
   443                 repo.setparents(p1, parents[1])
   444                 repo.setparents(p1, parents[1])
   444             modified, added, removed, deleted = repo.status()[:4]
   445             modified, added, removed, deleted = repo.status()[:4]
   449                     date,
   450                     date,
   450                     extra=extra,
   451                     extra=extra,
   451                     editor=self.getcommiteditor(),
   452                     editor=self.getcommiteditor(),
   452                 )
   453                 )
   453                 if not n:
   454                 if not n:
   454                     raise error.Abort(_('commit failed'))
   455                     raise error.Abort(_(b'commit failed'))
   455                 if not merge:
   456                 if not merge:
   456                     self.transplants.set(n, node)
   457                     self.transplants.set(n, node)
   457             else:
   458             else:
   458                 n = None
   459                 n = None
   459             self.unlog()
   460             self.unlog()
   465             pass
   466             pass
   466 
   467 
   467     def stop(self, ui, repo):
   468     def stop(self, ui, repo):
   468         """logic to stop an interrupted transplant"""
   469         """logic to stop an interrupted transplant"""
   469         if self.canresume():
   470         if self.canresume():
   470             startctx = repo['.']
   471             startctx = repo[b'.']
   471             hg.updaterepo(repo, startctx.node(), overwrite=True)
   472             hg.updaterepo(repo, startctx.node(), overwrite=True)
   472             ui.status(_("stopped the interrupted transplant\n"))
   473             ui.status(_(b"stopped the interrupted transplant\n"))
   473             ui.status(
   474             ui.status(
   474                 _("working directory is now at %s\n") % startctx.hex()[:12]
   475                 _(b"working directory is now at %s\n") % startctx.hex()[:12]
   475             )
   476             )
   476             self.unlog()
   477             self.unlog()
   477             return 0
   478             return 0
   478 
   479 
   479     def readseries(self):
   480     def readseries(self):
   480         nodes = []
   481         nodes = []
   481         merges = []
   482         merges = []
   482         cur = nodes
   483         cur = nodes
   483         for line in self.opener.read('series').splitlines():
   484         for line in self.opener.read(b'series').splitlines():
   484             if line.startswith('# Merges'):
   485             if line.startswith(b'# Merges'):
   485                 cur = merges
   486                 cur = merges
   486                 continue
   487                 continue
   487             cur.append(revlog.bin(line))
   488             cur.append(revlog.bin(line))
   488 
   489 
   489         return (nodes, merges)
   490         return (nodes, merges)
   492         if not revmap:
   493         if not revmap:
   493             return
   494             return
   494 
   495 
   495         if not os.path.isdir(self.path):
   496         if not os.path.isdir(self.path):
   496             os.mkdir(self.path)
   497             os.mkdir(self.path)
   497         series = self.opener('series', 'w')
   498         series = self.opener(b'series', b'w')
   498         for rev in sorted(revmap):
   499         for rev in sorted(revmap):
   499             series.write(nodemod.hex(revmap[rev]) + '\n')
   500             series.write(nodemod.hex(revmap[rev]) + b'\n')
   500         if merges:
   501         if merges:
   501             series.write('# Merges\n')
   502             series.write(b'# Merges\n')
   502             for m in merges:
   503             for m in merges:
   503                 series.write(nodemod.hex(m) + '\n')
   504                 series.write(nodemod.hex(m) + b'\n')
   504         series.close()
   505         series.close()
   505 
   506 
   506     def parselog(self, fp):
   507     def parselog(self, fp):
   507         parents = []
   508         parents = []
   508         message = []
   509         message = []
   511         user = None
   512         user = None
   512         date = None
   513         date = None
   513         for line in fp.read().splitlines():
   514         for line in fp.read().splitlines():
   514             if inmsg:
   515             if inmsg:
   515                 message.append(line)
   516                 message.append(line)
   516             elif line.startswith('# User '):
   517             elif line.startswith(b'# User '):
   517                 user = line[7:]
   518                 user = line[7:]
   518             elif line.startswith('# Date '):
   519             elif line.startswith(b'# Date '):
   519                 date = line[7:]
   520                 date = line[7:]
   520             elif line.startswith('# Node ID '):
   521             elif line.startswith(b'# Node ID '):
   521                 node = revlog.bin(line[10:])
   522                 node = revlog.bin(line[10:])
   522             elif line.startswith('# Parent '):
   523             elif line.startswith(b'# Parent '):
   523                 parents.append(revlog.bin(line[9:]))
   524                 parents.append(revlog.bin(line[9:]))
   524             elif not line.startswith('# '):
   525             elif not line.startswith(b'# '):
   525                 inmsg = True
   526                 inmsg = True
   526                 message.append(line)
   527                 message.append(line)
   527         if None in (user, date):
   528         if None in (user, date):
   528             raise error.Abort(_("filter corrupted changeset (no user or date)"))
   529             raise error.Abort(
   529         return (node, user, date, '\n'.join(message), parents)
   530                 _(b"filter corrupted changeset (no user or date)")
       
   531             )
       
   532         return (node, user, date, b'\n'.join(message), parents)
   530 
   533 
   531     def log(self, user, date, message, p1, p2, merge=False):
   534     def log(self, user, date, message, p1, p2, merge=False):
   532         '''journal changelog metadata for later recover'''
   535         '''journal changelog metadata for later recover'''
   533 
   536 
   534         if not os.path.isdir(self.path):
   537         if not os.path.isdir(self.path):
   535             os.mkdir(self.path)
   538             os.mkdir(self.path)
   536         fp = self.opener('journal', 'w')
   539         fp = self.opener(b'journal', b'w')
   537         fp.write('# User %s\n' % user)
   540         fp.write(b'# User %s\n' % user)
   538         fp.write('# Date %s\n' % date)
   541         fp.write(b'# Date %s\n' % date)
   539         fp.write('# Node ID %s\n' % nodemod.hex(p2))
   542         fp.write(b'# Node ID %s\n' % nodemod.hex(p2))
   540         fp.write('# Parent ' + nodemod.hex(p1) + '\n')
   543         fp.write(b'# Parent ' + nodemod.hex(p1) + b'\n')
   541         if merge:
   544         if merge:
   542             fp.write('# Parent ' + nodemod.hex(p2) + '\n')
   545             fp.write(b'# Parent ' + nodemod.hex(p2) + b'\n')
   543         fp.write(message.rstrip() + '\n')
   546         fp.write(message.rstrip() + b'\n')
   544         fp.close()
   547         fp.close()
   545 
   548 
   546     def readlog(self):
   549     def readlog(self):
   547         return self.parselog(self.opener('journal'))
   550         return self.parselog(self.opener(b'journal'))
   548 
   551 
   549     def unlog(self):
   552     def unlog(self):
   550         '''remove changelog journal'''
   553         '''remove changelog journal'''
   551         absdst = os.path.join(self.path, 'journal')
   554         absdst = os.path.join(self.path, b'journal')
   552         if os.path.exists(absdst):
   555         if os.path.exists(absdst):
   553             os.unlink(absdst)
   556             os.unlink(absdst)
   554 
   557 
   555     def transplantfilter(self, repo, source, root):
   558     def transplantfilter(self, repo, source, root):
   556         def matchfn(node):
   559         def matchfn(node):
   557             if self.applied(repo, node, root):
   560             if self.applied(repo, node, root):
   558                 return False
   561                 return False
   559             if source.changelog.parents(node)[1] != revlog.nullid:
   562             if source.changelog.parents(node)[1] != revlog.nullid:
   560                 return False
   563                 return False
   561             extra = source.changelog.read(node)[5]
   564             extra = source.changelog.read(node)[5]
   562             cnode = extra.get('transplant_source')
   565             cnode = extra.get(b'transplant_source')
   563             if cnode and self.applied(repo, cnode, root):
   566             if cnode and self.applied(repo, cnode, root):
   564                 return False
   567                 return False
   565             return True
   568             return True
   566 
   569 
   567         return matchfn
   570         return matchfn
   578     '''interactively transplant changesets'''
   581     '''interactively transplant changesets'''
   579     displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
   582     displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
   580     transplants = []
   583     transplants = []
   581     merges = []
   584     merges = []
   582     prompt = _(
   585     prompt = _(
   583         'apply changeset? [ynmpcq?]:'
   586         b'apply changeset? [ynmpcq?]:'
   584         '$$ &yes, transplant this changeset'
   587         b'$$ &yes, transplant this changeset'
   585         '$$ &no, skip this changeset'
   588         b'$$ &no, skip this changeset'
   586         '$$ &merge at this changeset'
   589         b'$$ &merge at this changeset'
   587         '$$ show &patch'
   590         b'$$ show &patch'
   588         '$$ &commit selected changesets'
   591         b'$$ &commit selected changesets'
   589         '$$ &quit and cancel transplant'
   592         b'$$ &quit and cancel transplant'
   590         '$$ &? (show this help)'
   593         b'$$ &? (show this help)'
   591     )
   594     )
   592     for node in nodes:
   595     for node in nodes:
   593         displayer.show(repo[node])
   596         displayer.show(repo[node])
   594         action = None
   597         action = None
   595         while not action:
   598         while not action:
   596             choice = ui.promptchoice(prompt)
   599             choice = ui.promptchoice(prompt)
   597             action = 'ynmpcq?'[choice : choice + 1]
   600             action = b'ynmpcq?'[choice : choice + 1]
   598             if action == '?':
   601             if action == b'?':
   599                 for c, t in ui.extractchoices(prompt)[1]:
   602                 for c, t in ui.extractchoices(prompt)[1]:
   600                     ui.write('%s: %s\n' % (c, t))
   603                     ui.write(b'%s: %s\n' % (c, t))
   601                 action = None
   604                 action = None
   602             elif action == 'p':
   605             elif action == b'p':
   603                 parent = repo.changelog.parents(node)[0]
   606                 parent = repo.changelog.parents(node)[0]
   604                 for chunk in patch.diff(repo, parent, node):
   607                 for chunk in patch.diff(repo, parent, node):
   605                     ui.write(chunk)
   608                     ui.write(chunk)
   606                 action = None
   609                 action = None
   607         if action == 'y':
   610         if action == b'y':
   608             transplants.append(node)
   611             transplants.append(node)
   609         elif action == 'm':
   612         elif action == b'm':
   610             merges.append(node)
   613             merges.append(node)
   611         elif action == 'c':
   614         elif action == b'c':
   612             break
   615             break
   613         elif action == 'q':
   616         elif action == b'q':
   614             transplants = ()
   617             transplants = ()
   615             merges = ()
   618             merges = ()
   616             break
   619             break
   617     displayer.close()
   620     displayer.close()
   618     return (transplants, merges)
   621     return (transplants, merges)
   619 
   622 
   620 
   623 
   621 @command(
   624 @command(
   622     'transplant',
   625     b'transplant',
   623     [
   626     [
   624         ('s', 'source', '', _('transplant changesets from REPO'), _('REPO')),
       
   625         ('b', 'branch', [], _('use this source changeset as head'), _('REV')),
       
   626         (
   627         (
   627             'a',
   628             b's',
   628             'all',
   629             b'source',
       
   630             b'',
       
   631             _(b'transplant changesets from REPO'),
       
   632             _(b'REPO'),
       
   633         ),
       
   634         (
       
   635             b'b',
       
   636             b'branch',
       
   637             [],
       
   638             _(b'use this source changeset as head'),
       
   639             _(b'REV'),
       
   640         ),
       
   641         (
       
   642             b'a',
       
   643             b'all',
   629             None,
   644             None,
   630             _('pull all changesets up to the --branch revisions'),
   645             _(b'pull all changesets up to the --branch revisions'),
   631         ),
   646         ),
   632         ('p', 'prune', [], _('skip over REV'), _('REV')),
   647         (b'p', b'prune', [], _(b'skip over REV'), _(b'REV')),
   633         ('m', 'merge', [], _('merge at REV'), _('REV')),
   648         (b'm', b'merge', [], _(b'merge at REV'), _(b'REV')),
   634         (
   649         (
   635             '',
   650             b'',
   636             'parent',
   651             b'parent',
   637             '',
   652             b'',
   638             _('parent to choose when transplanting merge'),
   653             _(b'parent to choose when transplanting merge'),
   639             _('REV'),
   654             _(b'REV'),
   640         ),
   655         ),
   641         ('e', 'edit', False, _('invoke editor on commit messages')),
   656         (b'e', b'edit', False, _(b'invoke editor on commit messages')),
   642         ('', 'log', None, _('append transplant info to log message')),
   657         (b'', b'log', None, _(b'append transplant info to log message')),
   643         ('', 'stop', False, _('stop interrupted transplant')),
   658         (b'', b'stop', False, _(b'stop interrupted transplant')),
   644         (
   659         (
   645             'c',
   660             b'c',
   646             'continue',
   661             b'continue',
   647             None,
   662             None,
   648             _('continue last transplant session ' 'after fixing conflicts'),
   663             _(b'continue last transplant session ' b'after fixing conflicts'),
   649         ),
   664         ),
   650         ('', 'filter', '', _('filter changesets through command'), _('CMD')),
   665         (
       
   666             b'',
       
   667             b'filter',
       
   668             b'',
       
   669             _(b'filter changesets through command'),
       
   670             _(b'CMD'),
       
   671         ),
   651     ],
   672     ],
   652     _('hg transplant [-s REPO] [-b BRANCH [-a]] [-p REV] ' '[-m REV] [REV]...'),
   673     _(
       
   674         b'hg transplant [-s REPO] [-b BRANCH [-a]] [-p REV] '
       
   675         b'[-m REV] [REV]...'
       
   676     ),
   653     helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
   677     helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
   654 )
   678 )
   655 def transplant(ui, repo, *revs, **opts):
   679 def transplant(ui, repo, *revs, **opts):
   656     '''transplant changesets from another branch
   680     '''transplant changesets from another branch
   657 
   681 
   726         for node in repo.changelog.nodesbetween(ancestors, heads)[0]:
   750         for node in repo.changelog.nodesbetween(ancestors, heads)[0]:
   727             if match(node):
   751             if match(node):
   728                 yield node
   752                 yield node
   729 
   753 
   730     def checkopts(opts, revs):
   754     def checkopts(opts, revs):
   731         if opts.get('continue'):
   755         if opts.get(b'continue'):
   732             if opts.get('branch') or opts.get('all') or opts.get('merge'):
   756             if opts.get(b'branch') or opts.get(b'all') or opts.get(b'merge'):
   733                 raise error.Abort(
   757                 raise error.Abort(
   734                     _(
   758                     _(
   735                         '--continue is incompatible with '
   759                         b'--continue is incompatible with '
   736                         '--branch, --all and --merge'
   760                         b'--branch, --all and --merge'
   737                     )
   761                     )
   738                 )
   762                 )
   739             return
   763             return
   740         if opts.get('stop'):
   764         if opts.get(b'stop'):
   741             if opts.get('branch') or opts.get('all') or opts.get('merge'):
   765             if opts.get(b'branch') or opts.get(b'all') or opts.get(b'merge'):
   742                 raise error.Abort(
   766                 raise error.Abort(
   743                     _(
   767                     _(
   744                         '--stop is incompatible with '
   768                         b'--stop is incompatible with '
   745                         '--branch, --all and --merge'
   769                         b'--branch, --all and --merge'
   746                     )
   770                     )
   747                 )
   771                 )
   748             return
   772             return
   749         if not (
   773         if not (
   750             opts.get('source')
   774             opts.get(b'source')
   751             or revs
   775             or revs
   752             or opts.get('merge')
   776             or opts.get(b'merge')
   753             or opts.get('branch')
   777             or opts.get(b'branch')
   754         ):
   778         ):
   755             raise error.Abort(
   779             raise error.Abort(
   756                 _(
   780                 _(
   757                     'no source URL, branch revision, or revision '
   781                     b'no source URL, branch revision, or revision '
   758                     'list provided'
   782                     b'list provided'
   759                 )
   783                 )
   760             )
   784             )
   761         if opts.get('all'):
   785         if opts.get(b'all'):
   762             if not opts.get('branch'):
   786             if not opts.get(b'branch'):
   763                 raise error.Abort(_('--all requires a branch revision'))
   787                 raise error.Abort(_(b'--all requires a branch revision'))
   764             if revs:
   788             if revs:
   765                 raise error.Abort(
   789                 raise error.Abort(
   766                     _('--all is incompatible with a ' 'revision list')
   790                     _(b'--all is incompatible with a ' b'revision list')
   767                 )
   791                 )
   768 
   792 
   769     opts = pycompat.byteskwargs(opts)
   793     opts = pycompat.byteskwargs(opts)
   770     checkopts(opts, revs)
   794     checkopts(opts, revs)
   771 
   795 
   772     if not opts.get('log'):
   796     if not opts.get(b'log'):
   773         # deprecated config: transplant.log
   797         # deprecated config: transplant.log
   774         opts['log'] = ui.config('transplant', 'log')
   798         opts[b'log'] = ui.config(b'transplant', b'log')
   775     if not opts.get('filter'):
   799     if not opts.get(b'filter'):
   776         # deprecated config: transplant.filter
   800         # deprecated config: transplant.filter
   777         opts['filter'] = ui.config('transplant', 'filter')
   801         opts[b'filter'] = ui.config(b'transplant', b'filter')
   778 
   802 
   779     tp = transplanter(ui, repo, opts)
   803     tp = transplanter(ui, repo, opts)
   780 
   804 
   781     p1 = repo.dirstate.p1()
   805     p1 = repo.dirstate.p1()
   782     if len(repo) > 0 and p1 == revlog.nullid:
   806     if len(repo) > 0 and p1 == revlog.nullid:
   783         raise error.Abort(_('no revision checked out'))
   807         raise error.Abort(_(b'no revision checked out'))
   784     if opts.get('continue'):
   808     if opts.get(b'continue'):
   785         if not tp.canresume():
   809         if not tp.canresume():
   786             raise error.Abort(_('no transplant to continue'))
   810             raise error.Abort(_(b'no transplant to continue'))
   787     elif opts.get('stop'):
   811     elif opts.get(b'stop'):
   788         if not tp.canresume():
   812         if not tp.canresume():
   789             raise error.Abort(_('no interrupted transplant found'))
   813             raise error.Abort(_(b'no interrupted transplant found'))
   790         return tp.stop(ui, repo)
   814         return tp.stop(ui, repo)
   791     else:
   815     else:
   792         cmdutil.checkunfinished(repo)
   816         cmdutil.checkunfinished(repo)
   793         cmdutil.bailifchanged(repo)
   817         cmdutil.bailifchanged(repo)
   794 
   818 
   795     sourcerepo = opts.get('source')
   819     sourcerepo = opts.get(b'source')
   796     if sourcerepo:
   820     if sourcerepo:
   797         peer = hg.peer(repo, opts, ui.expandpath(sourcerepo))
   821         peer = hg.peer(repo, opts, ui.expandpath(sourcerepo))
   798         heads = pycompat.maplist(peer.lookup, opts.get('branch', ()))
   822         heads = pycompat.maplist(peer.lookup, opts.get(b'branch', ()))
   799         target = set(heads)
   823         target = set(heads)
   800         for r in revs:
   824         for r in revs:
   801             try:
   825             try:
   802                 target.add(peer.lookup(r))
   826                 target.add(peer.lookup(r))
   803             except error.RepoError:
   827             except error.RepoError:
   805         source, csets, cleanupfn = bundlerepo.getremotechanges(
   829         source, csets, cleanupfn = bundlerepo.getremotechanges(
   806             ui, repo, peer, onlyheads=sorted(target), force=True
   830             ui, repo, peer, onlyheads=sorted(target), force=True
   807         )
   831         )
   808     else:
   832     else:
   809         source = repo
   833         source = repo
   810         heads = pycompat.maplist(source.lookup, opts.get('branch', ()))
   834         heads = pycompat.maplist(source.lookup, opts.get(b'branch', ()))
   811         cleanupfn = None
   835         cleanupfn = None
   812 
   836 
   813     try:
   837     try:
   814         if opts.get('continue'):
   838         if opts.get(b'continue'):
   815             tp.resume(repo, source, opts)
   839             tp.resume(repo, source, opts)
   816             return
   840             return
   817 
   841 
   818         tf = tp.transplantfilter(repo, source, p1)
   842         tf = tp.transplantfilter(repo, source, p1)
   819         if opts.get('prune'):
   843         if opts.get(b'prune'):
   820             prune = set(
   844             prune = set(
   821                 source[r].node()
   845                 source[r].node()
   822                 for r in scmutil.revrange(source, opts.get('prune'))
   846                 for r in scmutil.revrange(source, opts.get(b'prune'))
   823             )
   847             )
   824             matchfn = lambda x: tf(x) and x not in prune
   848             matchfn = lambda x: tf(x) and x not in prune
   825         else:
   849         else:
   826             matchfn = tf
   850             matchfn = tf
   827         merges = pycompat.maplist(source.lookup, opts.get('merge', ()))
   851         merges = pycompat.maplist(source.lookup, opts.get(b'merge', ()))
   828         revmap = {}
   852         revmap = {}
   829         if revs:
   853         if revs:
   830             for r in scmutil.revrange(source, revs):
   854             for r in scmutil.revrange(source, revs):
   831                 revmap[int(r)] = source[r].node()
   855                 revmap[int(r)] = source[r].node()
   832         elif opts.get('all') or not merges:
   856         elif opts.get(b'all') or not merges:
   833             if source != repo:
   857             if source != repo:
   834                 alltransplants = incwalk(source, csets, match=matchfn)
   858                 alltransplants = incwalk(source, csets, match=matchfn)
   835             else:
   859             else:
   836                 alltransplants = transplantwalk(
   860                 alltransplants = transplantwalk(
   837                     source, p1, heads, match=matchfn
   861                     source, p1, heads, match=matchfn
   838                 )
   862                 )
   839             if opts.get('all'):
   863             if opts.get(b'all'):
   840                 revs = alltransplants
   864                 revs = alltransplants
   841             else:
   865             else:
   842                 revs, newmerges = browserevs(ui, source, alltransplants, opts)
   866                 revs, newmerges = browserevs(ui, source, alltransplants, opts)
   843                 merges.extend(newmerges)
   867                 merges.extend(newmerges)
   844             for r in revs:
   868             for r in revs:
   861 
   885 
   862 
   886 
   863 revsetpredicate = registrar.revsetpredicate()
   887 revsetpredicate = registrar.revsetpredicate()
   864 
   888 
   865 
   889 
   866 @revsetpredicate('transplanted([set])')
   890 @revsetpredicate(b'transplanted([set])')
   867 def revsettransplanted(repo, subset, x):
   891 def revsettransplanted(repo, subset, x):
   868     """Transplanted changesets in set, or all transplanted changesets.
   892     """Transplanted changesets in set, or all transplanted changesets.
   869     """
   893     """
   870     if x:
   894     if x:
   871         s = revset.getset(repo, subset, x)
   895         s = revset.getset(repo, subset, x)
   872     else:
   896     else:
   873         s = subset
   897         s = subset
   874     return smartset.baseset(
   898     return smartset.baseset(
   875         [r for r in s if repo[r].extra().get('transplant_source')]
   899         [r for r in s if repo[r].extra().get(b'transplant_source')]
   876     )
   900     )
   877 
   901 
   878 
   902 
   879 templatekeyword = registrar.templatekeyword()
   903 templatekeyword = registrar.templatekeyword()
   880 
   904 
   881 
   905 
   882 @templatekeyword('transplanted', requires={'ctx'})
   906 @templatekeyword(b'transplanted', requires={b'ctx'})
   883 def kwtransplanted(context, mapping):
   907 def kwtransplanted(context, mapping):
   884     """String. The node identifier of the transplanted
   908     """String. The node identifier of the transplanted
   885     changeset if any."""
   909     changeset if any."""
   886     ctx = context.resource(mapping, 'ctx')
   910     ctx = context.resource(mapping, b'ctx')
   887     n = ctx.extra().get('transplant_source')
   911     n = ctx.extra().get(b'transplant_source')
   888     return n and nodemod.hex(n) or ''
   912     return n and nodemod.hex(n) or b''
   889 
   913 
   890 
   914 
   891 def extsetup(ui):
   915 def extsetup(ui):
   892     statemod.addunfinished(
   916     statemod.addunfinished(
   893         'transplant',
   917         b'transplant',
   894         fname='transplant/journal',
   918         fname=b'transplant/journal',
   895         clearable=True,
   919         clearable=True,
   896         continuefunc=continuecmd,
   920         continuefunc=continuecmd,
   897         statushint=_(
   921         statushint=_(
   898             'To continue:    hg transplant --continue\n'
   922             b'To continue:    hg transplant --continue\n'
   899             'To stop:        hg transplant --stop'
   923             b'To stop:        hg transplant --stop'
   900         ),
   924         ),
   901         cmdhint=_("use 'hg transplant --continue' or 'hg transplant --stop'"),
   925         cmdhint=_(b"use 'hg transplant --continue' or 'hg transplant --stop'"),
   902     )
   926     )
   903 
   927 
   904 
   928 
   905 # tell hggettext to extract docstrings from these functions:
   929 # tell hggettext to extract docstrings from these functions:
   906 i18nfunctions = [revsettransplanted, kwtransplanted]
   930 i18nfunctions = [revsettransplanted, kwtransplanted]