fix: recompute command path lengths when a subtree is reparented - #2473
fix: recompute command path lengths when a subtree is reparented#2473r0h1tb wants to merge 2 commits into
Conversation
AddCommand caches len(x.CommandPath()) on the parent at the moment a
subcommand is attached. CommandPath() is derived by walking the parent
chain, so attaching a subtree somewhere new lengthens the path of every
command beneath it and invalidates whatever was cached earlier.
Building bottom-up therefore rendered different help text than the same
tree built top-down:
sub.AddCommand(t1); sub.AddCommand(t2); root.AddCommand(sub)
caches "remote authentication" (21) on sub, while
root.AddCommand(sub); sub.AddCommand(t1); sub.AddCommand(t2)
caches "app remote authentication" (25). CommandPathPadding() reads that
value, so "Additional help topics" came out misaligned depending only on
construction order.
AddCommand now refreshes the cached path lengths for the attached command
and all of its descendants. Use and Name are unaffected by reparenting, so
only commandsMaxCommandPathLen is recomputed. RemoveCommand already did an
equivalent recompute for its own children.
Fixes spf13#2463
|
Thanks for the recursive refresh — I found the matching invalidation when a subtree is detached.
remote.AddCommand(authentication, tls)
root.AddCommand(remote)
root.RemoveCommand(remote)
// stale with this PR: "app remote authentication" length
// expected: len("remote authentication")
if got, want := tls.CommandPathPadding(), len(authentication.CommandPath()); got != want {
t.Fatalf("padding = %d, want %d", got, want)
}The minimal follow-up appears to be refreshing the detached subtree immediately after clearing its parent: command.parent = nil
command.recomputeCommandPathLengths()I verified this in Docker against the PR branch: the reproduction fails before that line and passes after it, along with |
RemoveCommand clears the detached command's parent and recomputes the
lengths the former parent caches for its remaining children, but never
refreshes the caches the detached subtree holds for its own children.
CommandPath() is derived from the parent chain, so detaching shortens it
for every command beneath the subtree — the mirror image of the attach
case fixed in the previous commit.
On main this is already wrong for a tree built top-down:
root.AddCommand(remote)
remote.AddCommand(authentication, tls)
root.RemoveCommand(remote)
// remote.commandsMaxCommandPathLen == len("app remote authentication")
// but the longest path beneath remote is now "remote authentication"
The bottom-up order happened to produce the right value on main only
because the cache was never refreshed on attach either. Now that
AddCommand refreshes it, both orders need the matching invalidation on
detach, so the recompute makes the two paths symmetric.
Reported by @eugene-panin in review of spf13#2473.
|
Good catch — thanks @eugene-panin, this reproduces exactly as you describe and the suggested line is the right fix. Pushed in 4a5f267. Behavioural delta: One correction to the framingThe detach staleness isn't only a regression from this PR — it's already present on root.AddCommand(remote)
remote.AddCommand(authentication, tls)
root.RemoveCommand(remote)
// main today: 25 (len("app remote authentication"))
// correct: 21 (len("remote authentication"))I verified that against an unmodified So this isn't papering over something the PR broke — Tests
Copy-pasteable proof the tests pin the bug rather than restate the fix — revert the source hunk only and keep the tests:
Deliberately left out
root.AddCommand(&Command{Use: "authentication"})
root.ResetCommands()
root.AddCommand(&Command{Use: "ab"})
// maxPathLen=18 maxUseLen=14 maxNameLen=14; correct would be 6/2/2That reproduces on |
Fixes #2463
Problem
AddCommandcacheslen(x.CommandPath())on the parent at the moment a subcommand is attached.CommandPath()is derived by walking the parent chain, so attaching a subtree somewhere new lengthens the path of every command beneath it — invalidating anything cached earlier.Two identical trees therefore render different help text depending only on construction order:
CommandPathPadding()reads that cached value, so the "Additional help topics" column comes out misaligned in the bottom-up case.Fix
AddCommandnow refreshes cached path lengths for the attached command and all of its descendants, via a smallrecomputeCommandPathLengthshelper.Two notes on scope:
commandsMaxCommandPathLenis recomputed.commandsMaxUseLenandcommandsMaxNameLenderive fromUseandName, which reparenting doesn't affect.RemoveCommandalready performs an equivalent recompute for its own children, so this bringsAddCommandin line with it.Tests
TestCommandPathPaddingIndependentOfAddCommandOrderbuilds the issue's exact tree both ways and asserts the cached length matches, that it equalslen("app remote authentication"), and that the rendered help output is byte-identical.TestCommandPathPaddingRecomputedForDeepSubtreecovers the multi-level case.On
mainboth fail:With this change
go test ./...is green acrosscobraandcobra/doc.gofmtandgo vetare clean.