You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PRIO 1. Strict predecessor of #247. The unified Parse(ctx, raw, opts) (*Path, error) infrastructure must be in place before azdo pipelines agent show can be implemented. This refactor introduces the parse infrastructure only — the new ParsePoolAgentTargetWithDefaultOrganization public wrapper for #247 is added as part of #247's own implementation work, keeping this issue a pure refactor with no new public API.
Why now
internal/cmd/util/scope.go currently exposes 6 near-duplicate parsers, three of which re-implement the same default-org fallback. Every new "target shape" requires either a new struct + parallel parser, or a generalization that pollutes the existing types. A single options-driven Parse function eliminates the duplication and makes the next nested-target case a one-line wrapper that whichever feature needs it can own and add itself — keeping refactors and feature work cleanly separated.
The actual file is only 265 lines, but the duplication makes it hard to read and impossible to extend safely.
Locked Decisions
#
Decision
Rationale
1
Single canonical type Path { Organization string; Project string; Targets []string } replaces the existing Scope and Target structs. Callers are updated to use *Path.
One shape for any input format. Targets []string generalizes naturally to nested targets (length 1 today, length 2 for pool/agent in #247, future length 3 if needed).
2
Single internal function Parse(ctx, raw, opts ParseOptions) (*Path, error) does all the work — splitting, trimming, validation, and default-org resolution.
The default-org fallback, segment validation, and whitespace handling live in one place.
The 6 existing functions and any future wrapper map to a single Parse(...) call.
4
The 6 existing public function names are preserved (ParseScope, ParseProjectScope, ParseOrganizationArg, ParseTarget, ParseTargetWithDefaultOrganization, ParseProjectTargetWithDefaultOrganization) as thin wrappers that return *Path.
Caller call sites need only a target.Target → path.Targets[0] rename; the function name stays the same so the diff is mechanical.
5
ResolveScopeDescriptor is unchanged (it is not a parser; it is a service-call wrapper that operates on already-parsed scope fields).
Orthogonal concern.
6
Variable names at call sites do not need to change (e.g. scope, err := util.ParseProjectScope(ctx, raw) keeps scope as the variable name even though its type changes from *Scope to *Path).
Reduces diff noise.
7
All existing scope_test.go cases remain valid with only a result.Target → result.Targets[0] field rename. New table-driven TestParse covers the same shapes via direct Parse(ctx, raw, opts) calls.
Test coverage is preserved and improved.
API Surface
// Path represents a parsed user-input path of the form// [ORGANIZATION[/PROJECT]]/TARGET[/SUBTARGET[/...]].// Organization is always populated after a successful Parse.typePathstruct {
OrganizationstringProjectstringTargets []string
}
// ParseOptions configures how a raw user input is split into a Path.typeParseOptionsstruct {
AllowImplicitOrgboolRequireProjectboolMinTargetsintMaxTargetsint
}
// Parse splits a raw user input into a Path according to opts.// Use the convenience wrappers for the common cases.funcParse(ctxCmdContext, rawstring, optsParseOptions) (*Path, error)
Convenience wrappers (all return *Path unless noted; all 6 are preserved as thin inline-able wrappers):
Wrapper
AllowImplicitOrg
RequireProject
Min/Max Targets
Returns
ParseScope(ctx, raw)
true
false
0/0
*Path
ParseProjectScope(ctx, raw)
true
true
0/0
*Path
ParseOrganizationArg(ctx, raw)
(via ParseScope + post-check that Project is empty)
target.Target → path.Targets[0] at ~20 call sites.
scope.Organization / scope.Project field access unchanged (names match on both *Scope and *Path).
All 6 existing public functions keep their names; only their return types change from *Scope/*Target to *Path (silent change at call sites that use field access).
No new public API is introduced by this refactor. Future wrappers (e.g. ParsePoolAgentTargetWithDefaultOrganization for feat: Implement azdo pipelines agent show command #247) are added by whichever feature needs them, as thin wrappers over Parse.
Test plan
All existing scope_test.go cases pass after the result.Target → result.Targets[0] rename.
New TestParse table-driven test: every (input, options) combination from the 6 existing wrappers, expressed as direct Parse(ctx, raw, opts) calls. Drives the same code paths the wrappers use.
New edge cases for Parse: empty input rejected when !AllowImplicitOrg; whitespace-only input; segments with only whitespace; ctx == nil when default org is needed.
Why now
internal/cmd/util/scope.gocurrently exposes 6 near-duplicate parsers, three of which re-implement the same default-org fallback. Every new "target shape" requires either a new struct + parallel parser, or a generalization that pollutes the existing types. A single options-drivenParsefunction eliminates the duplication and makes the next nested-target case a one-line wrapper that whichever feature needs it can own and add itself — keeping refactors and feature work cleanly separated.The actual file is only 265 lines, but the duplication makes it hard to read and impossible to extend safely.
Locked Decisions
Path { Organization string; Project string; Targets []string }replaces the existingScopeandTargetstructs. Callers are updated to use*Path.Targets []stringgeneralizes naturally to nested targets (length 1 today, length 2 forpool/agentin #247, future length 3 if needed).Parse(ctx, raw, opts ParseOptions) (*Path, error)does all the work — splitting, trimming, validation, and default-org resolution.ParseOptionsstruct:AllowImplicitOrg bool; RequireProject bool; MinTargets int; MaxTargets int.Parse(...)call.ParseScope,ParseProjectScope,ParseOrganizationArg,ParseTarget,ParseTargetWithDefaultOrganization,ParseProjectTargetWithDefaultOrganization) as thin wrappers that return*Path.target.Target→path.Targets[0]rename; the function name stays the same so the diff is mechanical.ResolveScopeDescriptoris unchanged (it is not a parser; it is a service-call wrapper that operates on already-parsed scope fields).scope, err := util.ParseProjectScope(ctx, raw)keepsscopeas the variable name even though its type changes from*Scopeto*Path).scope_test.gocases remain valid with only aresult.Target→result.Targets[0]field rename. New table-drivenTestParsecovers the same shapes via directParse(ctx, raw, opts)calls.API Surface
Convenience wrappers (all return
*Pathunless noted; all 6 are preserved as thin inline-able wrappers):ParseScope(ctx, raw)*PathParseProjectScope(ctx, raw)*PathParseOrganizationArg(ctx, raw)ParseScope+ post-check that Project is empty)(string, error)ParseTarget(raw)*PathParseTargetWithDefaultOrganization(ctx, raw)*PathParseProjectTargetWithDefaultOrganization(ctx, raw)*PathInternal implementation sketch
Migration impact
target.Target→path.Targets[0]at ~20 call sites.scope.Organization/scope.Projectfield access unchanged (names match on both*Scopeand*Path).*Scope/*Targetto*Path(silent change at call sites that use field access).internal/cmd/util/scope.go+internal/cmd/util/scope_test.go).ResolveScopeDescriptorcallers: no change.ParsePoolAgentTargetWithDefaultOrganizationfor feat: Implementazdo pipelines agent showcommand #247) are added by whichever feature needs them, as thin wrappers overParse.Test plan
scope_test.gocases pass after theresult.Target→result.Targets[0]rename.TestParsetable-driven test: every (input, options) combination from the 6 existing wrappers, expressed as directParse(ctx, raw, opts)calls. Drives the same code paths the wrappers use.Parse: empty input rejected when!AllowImplicitOrg; whitespace-only input; segments with only whitespace;ctx == nilwhen default org is needed.Tooling and Verification
gofmt/gofumpton touched filesgo test ./internal/cmd/util/...go test ./...make lintBlocks
azdo pipelines agent showcommand #247 (feat: Implement azdo pipelines agent show command) — requires the unifiedutil.Parse(ctx, raw, opts) (*Path, error)infrastructure introduced by this refactor. This is a strict predecessor relationship: feat: Implementazdo pipelines agent showcommand #247 cannot be implemented until this issue is closed. Note: the newParsePoolAgentTargetWithDefaultOrganizationpublic wrapper is added as part of feat: Implementazdo pipelines agent showcommand #247's own implementation, not by this refactor — keeping this issue a pure refactor with no new public API.Reference
internal/cmd/util/scope.go(265 lines).internal/cmd/util/scope_test.go(373 lines, 24 test cases).azdo pipelines showcommand #243, feat: Implementazdo pipelines pool showcommand #244, feat: Implementazdo pipelines queue showcommand #245, feat: Implementazdo pipelines build cancelcommand #252, feat: Implementazdo pipelines build queuecommand #253, feat: Implementazdo pipelines deletecommand #255, feat: Implementazdo pipelines updatecommand #257, feat: Implementazdo pipelines runcommand #258) all reuse the existingParseProjectTargetWithDefaultOrganization/ParseTargetWithDefaultOrganizationwrappers — they are unaffected by this refactor except for the type change of the returned value.