Patchwork dispatch: add a whitelist map of commands to implicitly loaded extensions

login
register
mail settings
Submitter Matt Harbison
Date April 12, 2018, 5:35 p.m.
Message ID <986b51f15e9bce19b2f6.1523554550@MATT7H-PC.attotech.com>
Download mbox | patch
Permalink /patch/30820/
State New
Headers show

Comments

Matt Harbison - April 12, 2018, 5:35 p.m.
# HG changeset patch
# User Matt Harbison <matt_harbison@yahoo.com>
# Date 1523332699 14400
#      Mon Apr 09 23:58:19 2018 -0400
# Node ID 986b51f15e9bce19b2f67573ff76612540320d1b
# Parent  2e0e61312a257708a70201427b31deba964e9b05
dispatch: add a whitelist map of commands to implicitly loaded extensions

This was Augie's idea[1] (or at least my interpretation of it).  The goal is to
be able to load the lfs extension without user intervention where it is both
harmless, and provides a user convenience.  The referenced thread discusses the
clone command and the cryptic error around that in some cases.  But the obvious
benefit is to be able transparently support cloning an lfs repository, or the
sudden transition to lfs on a pull, without bothering the user.  The choice to
use the lfs extension in the repository has already been made in both cases at
that point, and neither command can accidentally introduce the feature.

The implementation is perhaps a bit more than imagined in the thread, because I
think there may be room to allow extensions like 'share' to force lfs on too, in
order to make the extension usage more invisible.  The largefiles extension
could probably be given the same treatment.  For simplicity, I didn't bother
allowing external extensions to be loadable like this.  The mechanism used here
was pilfered from acl.ensureenabled().

[1] https://www.mercurial-scm.org/pipermail/mercurial-devel/2018-January/109851.html
Yuya Nishihara - April 13, 2018, 1:11 p.m.
On Thu, 12 Apr 2018 13:35:50 -0400, Matt Harbison wrote:
> # HG changeset patch
> # User Matt Harbison <matt_harbison@yahoo.com>
> # Date 1523332699 14400
> #      Mon Apr 09 23:58:19 2018 -0400
> # Node ID 986b51f15e9bce19b2f67573ff76612540320d1b
> # Parent  2e0e61312a257708a70201427b31deba964e9b05
> dispatch: add a whitelist map of commands to implicitly loaded extensions
> 
> This was Augie's idea[1] (or at least my interpretation of it).  The goal is to
> be able to load the lfs extension without user intervention where it is both
> harmless, and provides a user convenience.  The referenced thread discusses the
> clone command and the cryptic error around that in some cases.  But the obvious
> benefit is to be able transparently support cloning an lfs repository, or the
> sudden transition to lfs on a pull, without bothering the user.  The choice to
> use the lfs extension in the repository has already been made in both cases at
> that point, and neither command can accidentally introduce the feature.
> 
> The implementation is perhaps a bit more than imagined in the thread, because I
> think there may be room to allow extensions like 'share' to force lfs on too, in
> order to make the extension usage more invisible.  The largefiles extension
> could probably be given the same treatment.  For simplicity, I didn't bother
> allowing external extensions to be loadable like this.  The mechanism used here
> was pilfered from acl.ensureenabled().
> 
> [1] https://www.mercurial-scm.org/pipermail/mercurial-devel/2018-January/109851.html
> 
> diff --git a/mercurial/dispatch.py b/mercurial/dispatch.py
> --- a/mercurial/dispatch.py
> +++ b/mercurial/dispatch.py
> @@ -48,6 +48,15 @@ from .utils import (
>  
>  unrecoverablewrite = registrar.command.unrecoverablewrite
>  
> +# Map a command to a list of extensions that are forced on prior to running the
> +# command (unless it has been explicitly disabled.)  The idea is to help the
> +# user by enabling extensions implicitly when requirements may be added by a
> +# remote exchange, instead of erroring out after a partial exchange.
> +extensionwhitelist = {
> +    commands.clone: ['lfs'],
> +    commands.pull: ['lfs'],
> +}

Perhaps we can't use a command function as key since it may be wrapped by
extensions.

>  class request(object):
>      def __init__(self, args, ui=None, repo=None, fin=None, fout=None,
>                   ferr=None, prereposetups=None):
> @@ -843,6 +852,20 @@ def _dispatch(req):
>          fullargs = args
>          cmd, func, args, options, cmdoptions = _parse(lui, args)
>  
> +        # resolve aliases back to the core function
> +        entrypoint = func
> +        if isinstance(entrypoint, cmdalias):
> +            entrypoint = entrypoint.fn
> +
> +        if entrypoint in extensionwhitelist:
> +            configured = [ext for (ext, _path) in ui.configitems("extensions")]
> +            missing = [ext for ext in extensionwhitelist[entrypoint]
> +                       if ext not in configured]
> +            for ext in missing:
> +                ui.setconfig('extensions', ext, '', source='internal')
> +            if missing:
> +                extensions.loadall(ui, missing)

I'm -1 on this because loading extra Python module isn't always instant, and
the loaded extension persists forever, which can be a problem for command-server
process.
Matt Harbison - April 13, 2018, 1:35 p.m.
> On Apr 13, 2018, at 9:11 AM, Yuya Nishihara <yuya@tcha.org> wrote:
> 
>> On Thu, 12 Apr 2018 13:35:50 -0400, Matt Harbison wrote:
>> # HG changeset patch
>> # User Matt Harbison <matt_harbison@yahoo.com>
>> # Date 1523332699 14400
>> #      Mon Apr 09 23:58:19 2018 -0400
>> # Node ID 986b51f15e9bce19b2f67573ff76612540320d1b
>> # Parent  2e0e61312a257708a70201427b31deba964e9b05
>> dispatch: add a whitelist map of commands to implicitly loaded extensions
> 
>> class request(object):
>>     def __init__(self, args, ui=None, repo=None, fin=None, fout=None,
>>                  ferr=None, prereposetups=None):
>> @@ -843,6 +852,20 @@ def _dispatch(req):
>>         fullargs = args
>>         cmd, func, args, options, cmdoptions = _parse(lui, args)
>> 
>> +        # resolve aliases back to the core function
>> +        entrypoint = func
>> +        if isinstance(entrypoint, cmdalias):
>> +            entrypoint = entrypoint.fn
>> +
>> +        if entrypoint in extensionwhitelist:
>> +            configured = [ext for (ext, _path) in ui.configitems("extensions")]
>> +            missing = [ext for ext in extensionwhitelist[entrypoint]
>> +                       if ext not in configured]
>> +            for ext in missing:
>> +                ui.setconfig('extensions', ext, '', source='internal')
>> +            if missing:
>> +                extensions.loadall(ui, missing)
> 
> I'm -1 on this because loading extra Python module isn't always instant, and
> the loaded extension persists forever, which can be a problem for command-server
> process.

Good point. Any alternate ideas?  I wasn’t sure what the bundlepart idea would look like, because presumably this needs to be loaded pretty early in the startup process.

One thing I like about this is the user doesn’t have to do anything to switch.  I can send out an email saying “upgrade to 4.6”, convert on the server, and nothing changes for the developer. Otherwise, everyone has to enable the extension in their user settings, or alias clone and pull to enable it anyway.
Gregory Szorc - April 13, 2018, 7:10 p.m.
On Fri, Apr 13, 2018 at 6:35 AM, Matt Harbison <mharbison72@gmail.com>
wrote:

>
> > On Apr 13, 2018, at 9:11 AM, Yuya Nishihara <yuya@tcha.org> wrote:
> >
> >> On Thu, 12 Apr 2018 13:35:50 -0400, Matt Harbison wrote:
> >> # HG changeset patch
> >> # User Matt Harbison <matt_harbison@yahoo.com>
> >> # Date 1523332699 14400
> >> #      Mon Apr 09 23:58:19 2018 -0400
> >> # Node ID 986b51f15e9bce19b2f67573ff76612540320d1b
> >> # Parent  2e0e61312a257708a70201427b31deba964e9b05
> >> dispatch: add a whitelist map of commands to implicitly loaded
> extensions
> >
> >> class request(object):
> >>     def __init__(self, args, ui=None, repo=None, fin=None, fout=None,
> >>                  ferr=None, prereposetups=None):
> >> @@ -843,6 +852,20 @@ def _dispatch(req):
> >>         fullargs = args
> >>         cmd, func, args, options, cmdoptions = _parse(lui, args)
> >>
> >> +        # resolve aliases back to the core function
> >> +        entrypoint = func
> >> +        if isinstance(entrypoint, cmdalias):
> >> +            entrypoint = entrypoint.fn
> >> +
> >> +        if entrypoint in extensionwhitelist:
> >> +            configured = [ext for (ext, _path) in
> ui.configitems("extensions")]
> >> +            missing = [ext for ext in extensionwhitelist[entrypoint]
> >> +                       if ext not in configured]
> >> +            for ext in missing:
> >> +                ui.setconfig('extensions', ext, '', source='internal')
> >> +            if missing:
> >> +                extensions.loadall(ui, missing)
> >
> > I'm -1 on this because loading extra Python module isn't always instant,
> and
> > the loaded extension persists forever, which can be a problem for
> command-server
> > process.
>
> Good point. Any alternate ideas?  I wasn’t sure what the bundlepart idea
> would look like, because presumably this needs to be loaded pretty early in
> the startup process.
>
> One thing I like about this is the user doesn’t have to do anything to
> switch.  I can send out an email saying “upgrade to 4.6”, convert on the
> server, and nothing changes for the developer. Otherwise, everyone has to
> enable the extension in their user settings, or alias clone and pull to
> enable it anyway.
>

I want to add a "capabilities" mechanism to hg.peer() and hg.repository()
that makes the caller declare what features the returned repository must
have. This would allow callers to say things like "I only want a read-only
repo," "I need to be able to speak wire protocol command X," etc. These
capabilities would get passed down to the localrepo or peer layers for
evaluation, where they may result in the constructed instance being a
different type, having a different composition of methods, etc. This may
seem orthogonal to command dispatch. However, I would eventually like
@command's to self-declare what features they need. e.g. `log` would say it
needs a read-only repository. `update` would say it needs a repository with
a working directory. `commit` would say it needs a mutable repository. A
command to update the narrow profile would require a repository type that
can be narrowed. And the list goes on.

For your immediate use case, getting the extension tie-ins could be
difficult. Obviously the extension needs to be loaded in order for the
extension to declare a "capability" for a requested repo/peer instance.

What we may want instead is to key things off .hg/requires or a
to-be-invented supplemental requirements-like file that declares soft
features. localrepository.__init__ could then load trusted extensions at
repo open time if a requirements/capability was present/requested. i.e. if
you talk to an LFS server, write a semaphore somewhere into .hg/ and have
something in core look for that and automatically load lfs if present. This
would get you the "automatically enable LFS when talking to LFS servers"
benefits. Alternatively, you could move the client pieces of LFS into core
so no extension loading is needed. That may still require a semaphore
somewhere. I think that's the best long-term behavior. But I think we
should shore up the storage interfaces and figure out the narrow/partial
clone integration story before we do that. Maybe much of this work has
already been done?
Matt Harbison - April 14, 2018, 3:30 a.m.
On Fri, 13 Apr 2018 15:10:45 -0400, Gregory Szorc  
<gregory.szorc@gmail.com> wrote:

> On Fri, Apr 13, 2018 at 6:35 AM, Matt Harbison <mharbison72@gmail.com>
> wrote:
>
>>
>> > On Apr 13, 2018, at 9:11 AM, Yuya Nishihara <yuya@tcha.org> wrote:
>> >
>> >> On Thu, 12 Apr 2018 13:35:50 -0400, Matt Harbison wrote:
>> >> # HG changeset patch
>> >> # User Matt Harbison <matt_harbison@yahoo.com>
>> >> # Date 1523332699 14400
>> >> #      Mon Apr 09 23:58:19 2018 -0400
>> >> # Node ID 986b51f15e9bce19b2f67573ff76612540320d1b
>> >> # Parent  2e0e61312a257708a70201427b31deba964e9b05
>> >> dispatch: add a whitelist map of commands to implicitly loaded
>> extensions
>> >

>> One thing I like about this is the user doesn’t have to do anything to
>> switch.  I can send out an email saying “upgrade to 4.6”, convert on the
>> server, and nothing changes for the developer. Otherwise, everyone has  
>> to
>> enable the extension in their user settings, or alias clone and pull to
>> enable it anyway.
>>
>
> I want to add a "capabilities" mechanism to hg.peer() and hg.repository()
> that makes the caller declare what features the returned repository must
> have. This would allow callers to say things like "I only want a  
> read-only
> repo," "I need to be able to speak wire protocol command X," etc. These
> capabilities would get passed down to the localrepo or peer layers for
> evaluation, where they may result in the constructed instance being a
> different type, having a different composition of methods, etc. This may
> seem orthogonal to command dispatch. However, I would eventually like
> @command's to self-declare what features they need. e.g. `log` would say  
> it
> needs a read-only repository. `update` would say it needs a repository  
> with
> a working directory. `commit` would say it needs a mutable repository. A
> command to update the narrow profile would require a repository type that
> can be narrowed. And the list goes on.
>
> For your immediate use case, getting the extension tie-ins could be
> difficult. Obviously the extension needs to be loaded in order for the
> extension to declare a "capability" for a requested repo/peer instance.
>
> What we may want instead is to key things off .hg/requires or a
> to-be-invented supplemental requirements-like file that declares soft
> features. localrepository.__init__ could then load trusted extensions at
> repo open time if a requirements/capability was present/requested. i.e.  
> if
> you talk to an LFS server, write a semaphore somewhere into .hg/ and have
> something in core look for that and automatically load lfs if present.  
> This would get you the "automatically enable LFS when talking to LFS  
> servers"
> benefits.

Sounds promising.  One of the things the lfs extension is doing is writing  
'[extensions]\nlfs=' to the repo's local hgrc file in a commit hook (it  
should probably be handling transactions too).  So it would be nice if  
this was baked into the core somehow (largefiles could use exactly the  
same handling, and I'm assuming narrow will be similar).  It's also  
manually adding to the requires file, since that doesn't seem to be  
updated in exchange.

Assuming the semaphore is persistent, it could be beneficial to load  
extensions indicated by it around the normal load time (e.g., largefiles  
wraps the clone command and adds extra switches).

I'm not sure if this addresses Yuya's concern about long lived processes  
though.

> Alternatively, you could move the client pieces of LFS into core
> so no extension loading is needed.

That doesn't seem viable.  The revlog flagprocessors drag along all of the  
store code.  A commit hook and pretxnchangegroup hook scan the relevant  
csets to see if the requirement needs to be written out, and I can't  
justify inflicting that on all repos, unless there's an O(1) test in the  
changegroup/bundle that I'm not aware of.  And the prefetchfiles hook is  
needed to support `hg clone` and `hg pull -u`.

> That may still require a semaphore
> somewhere. I think that's the best long-term behavior. But I think we
> should shore up the storage interfaces and figure out the narrow/partial
> clone integration story before we do that. Maybe much of this work has
> already been done?

My backup plan of alias shadowing clone/pull to add '--config  
extensions.lfs=' was a bust too, but I can wait on this.  I'm having  
trouble digesting the sheer volume of changes, so I guess when you think  
this has been reworked enough, let me know.
Yuya Nishihara - April 14, 2018, 7:09 a.m.
On Fri, 13 Apr 2018 23:30:43 -0400, Matt Harbison wrote:
> On Fri, 13 Apr 2018 15:10:45 -0400, Gregory Szorc  
> <gregory.szorc@gmail.com> wrote:
> > What we may want instead is to key things off .hg/requires or a
> > to-be-invented supplemental requirements-like file that declares soft
> > features. localrepository.__init__ could then load trusted extensions at
> > repo open time if a requirements/capability was present/requested. i.e.  
> > if
> > you talk to an LFS server, write a semaphore somewhere into .hg/ and have
> > something in core look for that and automatically load lfs if present.  
> > This would get you the "automatically enable LFS when talking to LFS  
> > servers"
> > benefits.
> 
> Sounds promising.  One of the things the lfs extension is doing is writing  
> '[extensions]\nlfs=' to the repo's local hgrc file in a commit hook (it  
> should probably be handling transactions too).  So it would be nice if  
> this was baked into the core somehow (largefiles could use exactly the  
> same handling, and I'm assuming narrow will be similar).  It's also  
> manually adding to the requires file, since that doesn't seem to be  
> updated in exchange.
> 
> Assuming the semaphore is persistent, it could be beneficial to load  
> extensions indicated by it around the normal load time (e.g., largefiles  
> wraps the clone command and adds extra switches).
> 
> I'm not sure if this addresses Yuya's concern about long lived processes  
> though.

If the extensions to be loaded are bound to a repo, yes, that should be
fine. A well-designed extension should check the repo requirements. What
doesn't work in command server is to load extensions per-command basis.
Matt Harbison - April 14, 2018, 6:09 p.m.
On Sat, 14 Apr 2018 03:09:01 -0400, Yuya Nishihara <yuya@tcha.org> wrote:

> On Fri, 13 Apr 2018 23:30:43 -0400, Matt Harbison wrote:
>> On Fri, 13 Apr 2018 15:10:45 -0400, Gregory Szorc
>> <gregory.szorc@gmail.com> wrote:
>> > What we may want instead is to key things off .hg/requires or a
>> > to-be-invented supplemental requirements-like file that declares soft
>> > features. localrepository.__init__ could then load trusted extensions  
>> at
>> > repo open time if a requirements/capability was present/requested.  
>> i.e.
>> > if
>> > you talk to an LFS server, write a semaphore somewhere into .hg/ and  
>> have
>> > something in core look for that and automatically load lfs if present.
>> > This would get you the "automatically enable LFS when talking to LFS
>> > servers"
>> > benefits.
>>
>> Sounds promising.  One of the things the lfs extension is doing is  
>> writing
>> '[extensions]\nlfs=' to the repo's local hgrc file in a commit hook (it
>> should probably be handling transactions too).  So it would be nice if
>> this was baked into the core somehow (largefiles could use exactly the
>> same handling, and I'm assuming narrow will be similar).  It's also
>> manually adding to the requires file, since that doesn't seem to be
>> updated in exchange.
>>
>> Assuming the semaphore is persistent, it could be beneficial to load
>> extensions indicated by it around the normal load time (e.g., largefiles
>> wraps the clone command and adds extra switches).
>>
>> I'm not sure if this addresses Yuya's concern about long lived processes
>> though.
>
> If the extensions to be loaded are bound to a repo, yes, that should be
> fine. A well-designed extension should check the repo requirements. What
> doesn't work in command server is to load extensions per-command basis.

While "bound to a repo" makes it sound like the extension would be  
isolated and could unload if the repo is GC'd, what I don't get is what  
that phrase means for all of the module functions that are wrapped.
Yuya Nishihara - April 15, 2018, 1:20 p.m.
On Sat, 14 Apr 2018 14:09:42 -0400, Matt Harbison wrote:
> On Sat, 14 Apr 2018 03:09:01 -0400, Yuya Nishihara <yuya@tcha.org> wrote:
> > On Fri, 13 Apr 2018 23:30:43 -0400, Matt Harbison wrote:
> >> Assuming the semaphore is persistent, it could be beneficial to load
> >> extensions indicated by it around the normal load time (e.g., largefiles
> >> wraps the clone command and adds extra switches).
> >>
> >> I'm not sure if this addresses Yuya's concern about long lived processes
> >> though.
> >
> > If the extensions to be loaded are bound to a repo, yes, that should be
> > fine. A well-designed extension should check the repo requirements. What
> > doesn't work in command server is to load extensions per-command basis.
> 
> While "bound to a repo" makes it sound like the extension would be
> isolated and could unload if the repo is GC'd, what I don't get is what
> that phrase means for all of the module functions that are wrapped.

I mean extension authors (and command-server clients) have to take care of
that situation anyway, because extensions may be enabled by repo hgrc. The
simplest solution for command-server clients is to spawn processes per
repository, which is what TortoiseHg does.

Patch

diff --git a/mercurial/dispatch.py b/mercurial/dispatch.py
--- a/mercurial/dispatch.py
+++ b/mercurial/dispatch.py
@@ -48,6 +48,15 @@  from .utils import (
 
 unrecoverablewrite = registrar.command.unrecoverablewrite
 
+# Map a command to a list of extensions that are forced on prior to running the
+# command (unless it has been explicitly disabled.)  The idea is to help the
+# user by enabling extensions implicitly when requirements may be added by a
+# remote exchange, instead of erroring out after a partial exchange.
+extensionwhitelist = {
+    commands.clone: ['lfs'],
+    commands.pull: ['lfs'],
+}
+
 class request(object):
     def __init__(self, args, ui=None, repo=None, fin=None, fout=None,
                  ferr=None, prereposetups=None):
@@ -843,6 +852,20 @@  def _dispatch(req):
         fullargs = args
         cmd, func, args, options, cmdoptions = _parse(lui, args)
 
+        # resolve aliases back to the core function
+        entrypoint = func
+        if isinstance(entrypoint, cmdalias):
+            entrypoint = entrypoint.fn
+
+        if entrypoint in extensionwhitelist:
+            configured = [ext for (ext, _path) in ui.configitems("extensions")]
+            missing = [ext for ext in extensionwhitelist[entrypoint]
+                       if ext not in configured]
+            for ext in missing:
+                ui.setconfig('extensions', ext, '', source='internal')
+            if missing:
+                extensions.loadall(ui, missing)
+
         if options["config"] != req.earlyoptions["config"]:
             raise error.Abort(_("option --config may not be abbreviated!"))
         if options["cwd"] != req.earlyoptions["cwd"]:
diff --git a/tests/test-clonebundles.t b/tests/test-clonebundles.t
--- a/tests/test-clonebundles.t
+++ b/tests/test-clonebundles.t
@@ -26,12 +26,13 @@  Missing manifest should not result in se
   adding manifests
   adding file changes
   added 2 changesets with 2 changes to 2 files
+  calling hook pretxnchangegroup.lfs: hgext.lfs.checkrequireslfs
   new changesets 53245c60e682:aaff8d2ffbbf
 
   $ cat server/access.log
   * - - [*] "GET /?cmd=capabilities HTTP/1.1" 200 - (glob)
   $LOCALIP - - [$LOGDATE$] "GET /?cmd=batch HTTP/1.1" 200 - x-hgarg-1:cmds=heads+%3Bknown+nodes%3D x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
-  $LOCALIP - - [$LOGDATE$] "GET /?cmd=getbundle HTTP/1.1" 200 - x-hgarg-1:bookmarks=1&$USUAL_BUNDLE_CAPS$&cg=1&common=0000000000000000000000000000000000000000&heads=aaff8d2ffbbf07a46dd1f05d8ae7877e3f56e2a2&listkeys=bookmarks&phases=1 x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
+  $LOCALIP - - [$LOGDATE$] "GET /?cmd=getbundle HTTP/1.1" 200 - x-hgarg-1:bookmarks=1&bundlecaps=HG20%2Cbundle2%3DHG20%250Abookmarks%250Achangegroup%253D01%252C02%252C03%250Adigests%253Dmd5%252Csha1%252Csha512%250Aerror%253Dabort%252Cunsupportedcontent%252Cpushraced%252Cpushkey%250Ahgtagsfnodes%250Alistkeys%250Aphases%253Dheads%250Apushkey%250Aremote-changegroup%253Dhttp%252Chttps%250Arev-branch-cache%250Astream%253Dv2&cg=1&common=0000000000000000000000000000000000000000&heads=aaff8d2ffbbf07a46dd1f05d8ae7877e3f56e2a2&listkeys=bookmarks&phases=1 x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
 
 Empty manifest file results in retrieval
 (the extension only checks if the manifest file exists)
@@ -44,6 +45,7 @@  Empty manifest file results in retrieval
   adding manifests
   adding file changes
   added 2 changesets with 2 changes to 2 files
+  calling hook pretxnchangegroup.lfs: hgext.lfs.checkrequireslfs
   new changesets 53245c60e682:aaff8d2ffbbf
 
 Manifest file with invalid URL aborts
diff --git a/tests/test-http-bad-server.t b/tests/test-http-bad-server.t
--- a/tests/test-http-bad-server.t
+++ b/tests/test-http-bad-server.t
@@ -189,7 +189,7 @@  Failure to read getbundle HTTP request
   readline(304 from 65537) -> (30) GET /?cmd=getbundle HTTP/1.1\r\n
   readline(274 from -1) -> (27) Accept-Encoding: identity\r\n
   readline(247 from -1) -> (29) vary: X-HgArg-1,X-HgProto-1\r\n
-  readline(218 from -1) -> (218) x-hgarg-1: bookmarks=1&bundlecaps=HG20%2Cbundle2%3DHG20%250Abookmarks%250Achangegroup%253D01%252C02%250Adigests%253Dmd5%252Csha1%252Csha512%250Aerror%253Dabort%252Cunsupportedcontent%252Cpushraced%252Cpushkey%250Ahgtag
+  readline(218 from -1) -> (218) x-hgarg-1: bookmarks=1&bundlecaps=HG20%2Cbundle2%3DHG20%250Abookmarks%250Achangegroup%253D01%252C02%252C03%250Adigests%253Dmd5%252Csha1%252Csha512%250Aerror%253Dabort%252Cunsupportedcontent%252Cpushraced%252Cpushkey%25
   read limit reached; closing socket
 
   $ rm -f error.log
@@ -449,7 +449,7 @@  TODO this output is terrible
   readline(65537) -> (30) GET /?cmd=getbundle HTTP/1.1\r\n
   readline(-1) -> (27) Accept-Encoding: identity\r\n
   readline(-1) -> (29) vary: X-HgArg-1,X-HgProto-1\r\n
-  readline(-1) -> (461) x-hgarg-1: bookmarks=1&bundlecaps=HG20%2Cbundle2%3DHG20%250Abookmarks%250Achangegroup%253D01%252C02%250Adigests%253Dmd5%252Csha1%252Csha512%250Aerror%253Dabort%252Cunsupportedcontent%252Cpushraced%252Cpushkey%250Ahgtagsfnodes%250Alistkeys%250Apushkey%250Aremote-changegroup%253Dhttp%252Chttps%250Arev-branch-cache%250Astream%253Dv2&cg=1&common=0000000000000000000000000000000000000000&heads=96ee1d7354c4ad7372047672c36a1f561e3a6a4c&listkeys=phases%2Cbookmarks\r\n
+  readline(-1) -> (468) x-hgarg-1: bookmarks=1&bundlecaps=HG20%2Cbundle2%3DHG20%250Abookmarks%250Achangegroup%253D01%252C02%252C03%250Adigests%253Dmd5%252Csha1%252Csha512%250Aerror%253Dabort%252Cunsupportedcontent%252Cpushraced%252Cpushkey%250Ahgtagsfnodes%250Alistkeys%250Apushkey%250Aremote-changegroup%253Dhttp%252Chttps%250Arev-branch-cache%250Astream%253Dv2&cg=1&common=0000000000000000000000000000000000000000&heads=96ee1d7354c4ad7372047672c36a1f561e3a6a4c&listkeys=phases%2Cbookmarks\r\n
   readline(-1) -> (61) x-hgproto-1: 0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull\r\n
   readline(-1) -> (35) accept: application/mercurial-0.1\r\n
   readline(-1) -> (2?) host: localhost:$HGPORT\r\n (glob)
@@ -510,7 +510,7 @@  Server sends empty HTTP body for getbund
   readline(65537) -> (30) GET /?cmd=getbundle HTTP/1.1\r\n
   readline(-1) -> (27) Accept-Encoding: identity\r\n
   readline(-1) -> (29) vary: X-HgArg-1,X-HgProto-1\r\n
-  readline(-1) -> (461) x-hgarg-1: bookmarks=1&bundlecaps=HG20%2Cbundle2%3DHG20%250Abookmarks%250Achangegroup%253D01%252C02%250Adigests%253Dmd5%252Csha1%252Csha512%250Aerror%253Dabort%252Cunsupportedcontent%252Cpushraced%252Cpushkey%250Ahgtagsfnodes%250Alistkeys%250Apushkey%250Aremote-changegroup%253Dhttp%252Chttps%250Arev-branch-cache%250Astream%253Dv2&cg=1&common=0000000000000000000000000000000000000000&heads=96ee1d7354c4ad7372047672c36a1f561e3a6a4c&listkeys=phases%2Cbookmarks\r\n
+  readline(-1) -> (468) x-hgarg-1: bookmarks=1&bundlecaps=HG20%2Cbundle2%3DHG20%250Abookmarks%250Achangegroup%253D01%252C02%252C03%250Adigests%253Dmd5%252Csha1%252Csha512%250Aerror%253Dabort%252Cunsupportedcontent%252Cpushraced%252Cpushkey%250Ahgtagsfnodes%250Alistkeys%250Apushkey%250Aremote-changegroup%253Dhttp%252Chttps%250Arev-branch-cache%250Astream%253Dv2&cg=1&common=0000000000000000000000000000000000000000&heads=96ee1d7354c4ad7372047672c36a1f561e3a6a4c&listkeys=phases%2Cbookmarks\r\n
   readline(-1) -> (61) x-hgproto-1: 0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull\r\n
   readline(-1) -> (35) accept: application/mercurial-0.1\r\n
   readline(-1) -> (2?) host: localhost:$HGPORT\r\n (glob)
@@ -573,7 +573,7 @@  Server sends partial compression string
   readline(65537) -> (30) GET /?cmd=getbundle HTTP/1.1\r\n
   readline(-1) -> (27) Accept-Encoding: identity\r\n
   readline(-1) -> (29) vary: X-HgArg-1,X-HgProto-1\r\n
-  readline(-1) -> (461) x-hgarg-1: bookmarks=1&bundlecaps=HG20%2Cbundle2%3DHG20%250Abookmarks%250Achangegroup%253D01%252C02%250Adigests%253Dmd5%252Csha1%252Csha512%250Aerror%253Dabort%252Cunsupportedcontent%252Cpushraced%252Cpushkey%250Ahgtagsfnodes%250Alistkeys%250Apushkey%250Aremote-changegroup%253Dhttp%252Chttps%250Arev-branch-cache%250Astream%253Dv2&cg=1&common=0000000000000000000000000000000000000000&heads=96ee1d7354c4ad7372047672c36a1f561e3a6a4c&listkeys=phases%2Cbookmarks\r\n
+  readline(-1) -> (468) x-hgarg-1: bookmarks=1&bundlecaps=HG20%2Cbundle2%3DHG20%250Abookmarks%250Achangegroup%253D01%252C02%252C03%250Adigests%253Dmd5%252Csha1%252Csha512%250Aerror%253Dabort%252Cunsupportedcontent%252Cpushraced%252Cpushkey%250Ahgtagsfnodes%250Alistkeys%250Apushkey%250Aremote-changegroup%253Dhttp%252Chttps%250Arev-branch-cache%250Astream%253Dv2&cg=1&common=0000000000000000000000000000000000000000&heads=96ee1d7354c4ad7372047672c36a1f561e3a6a4c&listkeys=phases%2Cbookmarks\r\n
   readline(-1) -> (61) x-hgproto-1: 0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull\r\n
   readline(-1) -> (35) accept: application/mercurial-0.1\r\n
   readline(-1) -> (2?) host: localhost:$HGPORT\r\n (glob)
diff --git a/tests/test-http-proxy.t b/tests/test-http-proxy.t
--- a/tests/test-http-proxy.t
+++ b/tests/test-http-proxy.t
@@ -111,16 +111,16 @@  do not use the proxy if it is in the no 
   $LOCALIP - - [$LOGDATE$] "GET http://localhost:$HGPORT/?cmd=branchmap HTTP/1.1" - - x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
   $LOCALIP - - [$LOGDATE$] "GET http://localhost:$HGPORT/?cmd=stream_out HTTP/1.1" - - x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
   $LOCALIP - - [$LOGDATE$] "GET http://localhost:$HGPORT/?cmd=batch HTTP/1.1" - - x-hgarg-1:cmds=heads+%3Bknown+nodes%3D83180e7845de420a1bb46896fd5fe05294f8d629 x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
-  $LOCALIP - - [$LOGDATE$] "GET http://localhost:$HGPORT/?cmd=getbundle HTTP/1.1" - - x-hgarg-1:bookmarks=1&$USUAL_BUNDLE_CAPS$&cg=0&common=83180e7845de420a1bb46896fd5fe05294f8d629&heads=83180e7845de420a1bb46896fd5fe05294f8d629&listkeys=bookmarks&phases=1 x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
+  $LOCALIP - - [$LOGDATE$] "GET http://localhost:$HGPORT/?cmd=getbundle HTTP/1.1" - - x-hgarg-1:bookmarks=1&bundlecaps=HG20%2Cbundle2%3DHG20%250Abookmarks%250Achangegroup%253D01%252C02%252C03%250Adigests%253Dmd5%252Csha1%252Csha512%250Aerror%253Dabort%252Cunsupportedcontent%252Cpushraced%252Cpushkey%250Ahgtagsfnodes%250Alistkeys%250Aphases%253Dheads%250Apushkey%250Aremote-changegroup%253Dhttp%252Chttps%250Arev-branch-cache%250Astream%253Dv2&cg=0&common=83180e7845de420a1bb46896fd5fe05294f8d629&heads=83180e7845de420a1bb46896fd5fe05294f8d629&listkeys=bookmarks&phases=1 x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
   * - - [*] "GET http://localhost:$HGPORT/?cmd=capabilities HTTP/1.1" - - (glob)
   $LOCALIP - - [$LOGDATE$] "GET http://localhost:$HGPORT/?cmd=batch HTTP/1.1" - - x-hgarg-1:cmds=heads+%3Bknown+nodes%3D x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
-  $LOCALIP - - [$LOGDATE$] "GET http://localhost:$HGPORT/?cmd=getbundle HTTP/1.1" - - x-hgarg-1:bookmarks=1&$USUAL_BUNDLE_CAPS$&cg=1&common=0000000000000000000000000000000000000000&heads=83180e7845de420a1bb46896fd5fe05294f8d629&listkeys=bookmarks&phases=1 x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
+  $LOCALIP - - [$LOGDATE$] "GET http://localhost:$HGPORT/?cmd=getbundle HTTP/1.1" - - x-hgarg-1:bookmarks=1&bundlecaps=HG20%2Cbundle2%3DHG20%250Abookmarks%250Achangegroup%253D01%252C02%252C03%250Adigests%253Dmd5%252Csha1%252Csha512%250Aerror%253Dabort%252Cunsupportedcontent%252Cpushraced%252Cpushkey%250Ahgtagsfnodes%250Alistkeys%250Aphases%253Dheads%250Apushkey%250Aremote-changegroup%253Dhttp%252Chttps%250Arev-branch-cache%250Astream%253Dv2&cg=1&common=0000000000000000000000000000000000000000&heads=83180e7845de420a1bb46896fd5fe05294f8d629&listkeys=bookmarks&phases=1 x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
   * - - [*] "GET http://localhost:$HGPORT/?cmd=capabilities HTTP/1.1" - - (glob)
   $LOCALIP - - [$LOGDATE$] "GET http://localhost:$HGPORT/?cmd=batch HTTP/1.1" - - x-hgarg-1:cmds=heads+%3Bknown+nodes%3D x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
-  $LOCALIP - - [$LOGDATE$] "GET http://localhost:$HGPORT/?cmd=getbundle HTTP/1.1" - - x-hgarg-1:bookmarks=1&$USUAL_BUNDLE_CAPS$&cg=1&common=0000000000000000000000000000000000000000&heads=83180e7845de420a1bb46896fd5fe05294f8d629&listkeys=bookmarks&phases=1 x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
+  $LOCALIP - - [$LOGDATE$] "GET http://localhost:$HGPORT/?cmd=getbundle HTTP/1.1" - - x-hgarg-1:bookmarks=1&bundlecaps=HG20%2Cbundle2%3DHG20%250Abookmarks%250Achangegroup%253D01%252C02%252C03%250Adigests%253Dmd5%252Csha1%252Csha512%250Aerror%253Dabort%252Cunsupportedcontent%252Cpushraced%252Cpushkey%250Ahgtagsfnodes%250Alistkeys%250Aphases%253Dheads%250Apushkey%250Aremote-changegroup%253Dhttp%252Chttps%250Arev-branch-cache%250Astream%253Dv2&cg=1&common=0000000000000000000000000000000000000000&heads=83180e7845de420a1bb46896fd5fe05294f8d629&listkeys=bookmarks&phases=1 x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
   * - - [*] "GET http://localhost:$HGPORT/?cmd=capabilities HTTP/1.1" - - (glob)
   $LOCALIP - - [$LOGDATE$] "GET http://localhost:$HGPORT/?cmd=batch HTTP/1.1" - - x-hgarg-1:cmds=heads+%3Bknown+nodes%3D x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
-  $LOCALIP - - [$LOGDATE$] "GET http://localhost:$HGPORT/?cmd=getbundle HTTP/1.1" - - x-hgarg-1:bookmarks=1&$USUAL_BUNDLE_CAPS$&cg=1&common=0000000000000000000000000000000000000000&heads=83180e7845de420a1bb46896fd5fe05294f8d629&listkeys=bookmarks&phases=1 x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
+  $LOCALIP - - [$LOGDATE$] "GET http://localhost:$HGPORT/?cmd=getbundle HTTP/1.1" - - x-hgarg-1:bookmarks=1&bundlecaps=HG20%2Cbundle2%3DHG20%250Abookmarks%250Achangegroup%253D01%252C02%252C03%250Adigests%253Dmd5%252Csha1%252Csha512%250Aerror%253Dabort%252Cunsupportedcontent%252Cpushraced%252Cpushkey%250Ahgtagsfnodes%250Alistkeys%250Aphases%253Dheads%250Apushkey%250Aremote-changegroup%253Dhttp%252Chttps%250Arev-branch-cache%250Astream%253Dv2&cg=1&common=0000000000000000000000000000000000000000&heads=83180e7845de420a1bb46896fd5fe05294f8d629&listkeys=bookmarks&phases=1 x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
   * - - [*] "GET http://localhost:$HGPORT/?cmd=capabilities HTTP/1.1" - - (glob)
   $LOCALIP - - [$LOGDATE$] "GET http://localhost:$HGPORT/?cmd=batch HTTP/1.1" - - x-hgarg-1:cmds=heads+%3Bknown+nodes%3D x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
-  $LOCALIP - - [$LOGDATE$] "GET http://localhost:$HGPORT/?cmd=getbundle HTTP/1.1" - - x-hgarg-1:bookmarks=1&$USUAL_BUNDLE_CAPS$&cg=1&common=0000000000000000000000000000000000000000&heads=83180e7845de420a1bb46896fd5fe05294f8d629&listkeys=bookmarks&phases=1 x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
+  $LOCALIP - - [$LOGDATE$] "GET http://localhost:$HGPORT/?cmd=getbundle HTTP/1.1" - - x-hgarg-1:bookmarks=1&bundlecaps=HG20%2Cbundle2%3DHG20%250Abookmarks%250Achangegroup%253D01%252C02%252C03%250Adigests%253Dmd5%252Csha1%252Csha512%250Aerror%253Dabort%252Cunsupportedcontent%252Cpushraced%252Cpushkey%250Ahgtagsfnodes%250Alistkeys%250Aphases%253Dheads%250Apushkey%250Aremote-changegroup%253Dhttp%252Chttps%250Arev-branch-cache%250Astream%253Dv2&cg=1&common=0000000000000000000000000000000000000000&heads=83180e7845de420a1bb46896fd5fe05294f8d629&listkeys=bookmarks&phases=1 x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
diff --git a/tests/test-http.t b/tests/test-http.t
--- a/tests/test-http.t
+++ b/tests/test-http.t
@@ -381,11 +381,11 @@  test http authentication
   "GET /?cmd=branchmap HTTP/1.1" 200 - x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (no-reposimplestore !)
   "GET /?cmd=stream_out HTTP/1.1" 200 - x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (no-reposimplestore !)
   "GET /?cmd=batch HTTP/1.1" 200 - x-hgarg-1:cmds=heads+%3Bknown+nodes%3D5fed3813f7f5e1824344fdc9cf8f63bb662c292d x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (no-reposimplestore !)
-  "GET /?cmd=getbundle HTTP/1.1" 200 - x-hgarg-1:bookmarks=1&$USUAL_BUNDLE_CAPS$&cg=0&common=5fed3813f7f5e1824344fdc9cf8f63bb662c292d&heads=5fed3813f7f5e1824344fdc9cf8f63bb662c292d&listkeys=bookmarks&phases=1 x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (no-reposimplestore !)
+  "GET /?cmd=getbundle HTTP/1.1" 200 - x-hgarg-1:bookmarks=1&bundlecaps=HG20%2Cbundle2%3DHG20%250Abookmarks%250Achangegroup%253D01%252C02%252C03%250Adigests%253Dmd5%252Csha1%252Csha512%250Aerror%253Dabort%252Cunsupportedcontent%252Cpushraced%252Cpushkey%250Ahgtagsfnodes%250Alistkeys%250Aphases%253Dheads%250Apushkey%250Aremote-changegroup%253Dhttp%252Chttps%250Arev-branch-cache%250Astream%253Dv2&cg=0&common=5fed3813f7f5e1824344fdc9cf8f63bb662c292d&heads=5fed3813f7f5e1824344fdc9cf8f63bb662c292d&listkeys=bookmarks&phases=1 x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull
   "GET /?cmd=capabilities HTTP/1.1" 401 - (no-reposimplestore !)
   "GET /?cmd=capabilities HTTP/1.1" 200 - (no-reposimplestore !)
   "GET /?cmd=batch HTTP/1.1" 200 - x-hgarg-1:cmds=heads+%3Bknown+nodes%3D x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull
-  "GET /?cmd=getbundle HTTP/1.1" 200 - x-hgarg-1:bookmarks=1&$USUAL_BUNDLE_CAPS$&cg=1&common=0000000000000000000000000000000000000000&heads=5fed3813f7f5e1824344fdc9cf8f63bb662c292d&listkeys=bookmarks&phases=1 x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull
+  "GET /?cmd=getbundle HTTP/1.1" 200 - x-hgarg-1:bookmarks=1&bundlecaps=HG20%2Cbundle2%3DHG20%250Abookmarks%250Achangegroup%253D01%252C02%252C03%250Adigests%253Dmd5%252Csha1%252Csha512%250Aerror%253Dabort%252Cunsupportedcontent%252Cpushraced%252Cpushkey%250Ahgtagsfnodes%250Alistkeys%250Aphases%253Dheads%250Apushkey%250Aremote-changegroup%253Dhttp%252Chttps%250Arev-branch-cache%250Astream%253Dv2&cg=1&common=0000000000000000000000000000000000000000&heads=5fed3813f7f5e1824344fdc9cf8f63bb662c292d&listkeys=bookmarks&phases=1 x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull
   "GET /?cmd=capabilities HTTP/1.1" 401 -
   "GET /?cmd=capabilities HTTP/1.1" 401 -
   "GET /?cmd=capabilities HTTP/1.1" 403 -
diff --git a/tests/test-lfs-serve.t b/tests/test-lfs-serve.t
--- a/tests/test-lfs-serve.t
+++ b/tests/test-lfs-serve.t
@@ -42,6 +42,9 @@  masked by the Internal Server Error mess
   > push_ssl=False
   > EOF
 
+Save the config prior to enabling the extension.
+  $ cp $HGRCPATH $HGRCPATH.unset
+
 #if lfsremote-on
   $ hg --config extensions.lfs= -R server \
   >    serve -p $HGPORT -d --pid-file=hg.pid --errorlog=$TESTTMP/errors.log
@@ -184,6 +187,8 @@  lfs content, and the extension enabled.
   $ grep 'lfs' .hg/requires $SERVER_REQUIRES
   $TESTTMP/server/.hg/requires:lfs
 
+Dispatch won't load this extension if it has been explicitly disabled.
+
 TODO: fail more gracefully.
 
   $ hg clone -q http://localhost:$HGPORT $TESTTMP/client4_clone
@@ -194,6 +199,23 @@  TODO: fail more gracefully.
   $TESTTMP/server/.hg/requires:lfs
   [2]
 
+Dispatch will helpfully load this extension if it wasn't already, and wasn't
+explicitly disabled.  That allows the extension to be enabled in the local hgrc
+as a convenience.
+
+  $ HGRCPATH=$HGRCPATH.unset \
+  > hg clone -q http://localhost:$HGPORT $TESTTMP/client4b_clone
+
+  $ grep 'lfs' $TESTTMP/client4b_clone/.hg/requires $SERVER_REQUIRES
+  $TESTTMP/client4b_clone/.hg/requires:lfs
+  $TESTTMP/server/.hg/requires:lfs
+
+  $ HGRCPATH=$HGRCPATH.unset \
+  > hg -R $TESTTMP/client4b_clone config extensions --debug
+  lfs: assuming remote store: http://localhost:$HGPORT/.git/info/lfs
+  read config from: *.unset (glob)
+  $TESTTMP/client4b_clone/.hg/hgrc:*: extensions.lfs= (glob)
+
 TODO: fail more gracefully.
 
   $ hg init $TESTTMP/client4_pull
@@ -203,6 +225,25 @@  TODO: fail more gracefully.
   $ grep 'lfs' $TESTTMP/client4_pull/.hg/requires $SERVER_REQUIRES
   $TESTTMP/server/.hg/requires:lfs
 
+Dispatch will also enable the extension for aliased commands in the whitelist.
+
+  $ HGRCPATH=$HGRCPATH.unset \
+  > hg --config alias.mypull=pull -R $TESTTMP/client4_pull \
+  >    mypull -q http://localhost:$HGPORT
+
+  $ grep 'lfs' $TESTTMP/client4_pull/.hg/requires $SERVER_REQUIRES
+  $TESTTMP/client4_pull/.hg/requires:lfs
+  $TESTTMP/server/.hg/requires:lfs
+
+TODO: permanently enable the extension in the local hgrc when the requirement
+is written out.
+
+  $ HGRCPATH=$HGRCPATH.unset \
+  > hg -R $TESTTMP/client4_pull config extensions --debug
+  abort: repository requires features unknown to this Mercurial: lfs!
+  (see https://mercurial-scm.org/wiki/MissingRequirement for more information)
+  [255]
+
   $ hg identify http://localhost:$HGPORT
   03b080fa9d93
 
diff --git a/tests/test-obsolete-changeset-exchange.t b/tests/test-obsolete-changeset-exchange.t
--- a/tests/test-obsolete-changeset-exchange.t
+++ b/tests/test-obsolete-changeset-exchange.t
@@ -168,7 +168,8 @@  client only pulls down 1 changeset
   adding file changes
   adding foo revisions
   added 1 changesets with 1 changes to 1 files (+1 heads)
-  bundle2-input-part: total payload size 476
+  calling hook pretxnchangegroup.lfs: hgext.lfs.checkrequireslfs
+  bundle2-input-part: total payload size 486
   bundle2-input-part: "listkeys" (params: 1 mandatory) supported
   bundle2-input-part: "phase-heads" supported
   bundle2-input-part: total payload size 24
diff --git a/tests/test-ssh.t b/tests/test-ssh.t
--- a/tests/test-ssh.t
+++ b/tests/test-ssh.t
@@ -514,7 +514,7 @@  debug output
   no changes found
   devel-peer-request: getbundle
   devel-peer-request:   bookmarks: 1 bytes
-  devel-peer-request:   bundlecaps: 266 bytes
+  devel-peer-request:   bundlecaps: 271 bytes
   devel-peer-request:   cg: 1 bytes
   devel-peer-request:   common: 122 bytes
   devel-peer-request:   heads: 122 bytes