GitHub CLI (gh) — Interfaces#
Interface catalog#
Config#
- Package:
internal/gh - File:
internal/gh/gh.go:32 - Methods: 17 —
GetOrDefault,Set,AccessibleColors,AccessiblePrompter,Browser,ColorLabels,Editor,GitProtocol,HTTPUnixSocket,Pager,Prompt,PreferEditorPrompt,Spinner,Aliases() AliasConfig,Authentication() AuthConfig,CacheDir,Migrate,Version,Write - Purpose: The canonical contract for all persistent user configuration. Provides host-scoped key/value access plus typed convenience getters for every known setting key. Composes
AuthConfigandAliasConfigvia accessor methods rather than embedding. - Implementations:
internal/config(reads/writes~/.config/gh/*.ymlYAML viacli/go-gh/v2/pkg/config);ghmock.Config(generated mock viamoq) - Design quality: Broad — 17 methods for a general-purpose config store is expected given the scope. Splits auth and alias concerns into sub-interfaces returned by methods, which is a sound composition strategy. The
GetOrDefault/Setprimitives provide an escape hatch for keys that don’t have typed getters.
AuthConfig#
- Package:
internal/gh - File:
internal/gh/gh.go:103 - Methods: 13 —
HasActiveToken,ActiveToken,HasEnvToken,TokenFromKeyring,TokenFromKeyringForUser,ActiveUser,Hosts,DefaultHost,Login,SwitchUser,Logout,UsersForHost,TokenForUser; plus 3 test-only override setters:SetActiveToken,SetHosts,SetDefaultHost - Purpose: Isolates all authentication state mutations and queries: multi-host token resolution (env → keyring → file), user switching, and login/logout lifecycle. The test-only methods (
SetActiveToken,SetHosts,SetDefaultHost) are self-acknowledged design smell (“a design smell we should consider fixing”) — they bleed test seams into the production interface. - Implementations:
internal/config(concrete);ghmock.AuthConfig(moq-generated) - Design quality: ISP violation acknowledged by the team. The presence of test-only methods on the production interface is a known trade-off, prioritizing test simplicity over purity. Multi-host awareness is well-modeled; the fallback chain (env → keyring → file) is correctly encapsulated behind
ActiveToken.
AliasConfig#
- Package:
internal/gh - File:
internal/gh/gh.go:172 - Methods: 4 —
Get(alias) (string, error),Add(alias, expansion string),Delete(alias) error,All() map[string]string - Purpose: CRUD for user-defined command aliases (
gh alias set,gh alias list). Returned byConfig.Aliases(). - Implementations:
internal/config - Design quality: Tight, well-segregated. Four methods cover exactly the alias use case. Correctly separated from
Configrather than merged.
Migration#
- Package:
internal/gh - File:
internal/gh/gh.go:91 - Methods: 3 —
PreVersion() string,PostVersion() string,Do(*ghConfig.Config) error - Purpose: Strategy pattern for config schema upgrades. A migration declares the version it expects (
PreVersion) and the version it produces (PostVersion). Calling code checks version compatibility before invokingDo. Concrete implementation:migration.MultiAccount(single- to multi-account upgrade). - Implementations:
internal/config/migration.MultiAccount - Design quality: Clean strategy pattern. Version constraints prevent accidental double-application. Small interface (3 methods) — excellent ISP adherence.
ghrepo.Interface#
- Package:
internal/ghrepo - File:
internal/ghrepo/repo.go:14 - Methods: 3 —
RepoName() string,RepoOwner() string,RepoHost() string - Purpose: Canonical identity abstraction for a GitHub repository. Used as currency across the entire codebase — every command that needs a repo target accepts or produces this interface. The host field enables GHES (GitHub Enterprise Server) support transparently.
- Implementations:
ghrepo.ghRepo(private struct); also satisfied by any type with these three methods (implicit satisfaction used extensively in tests) - Design quality: Exemplary Go interface design — minimal (3 methods), named after behavior (
Interfaceis unusual but the package nameghrepoprovides context:ghrepo.Interface). The host field is the key insight — it would be easy to omit and then require retrofitting for GHES.
ExtensionManager#
- Package:
pkg/extensions - File:
pkg/extensions/extension.go:32 - Methods: 8 —
List() []Extension,Install(ghrepo.Interface, string) error,InstallLocal(dir string) error,Upgrade(name string, force bool) error,Remove(name string) error,Dispatch(args []string, stdin io.Reader, stdout, stderr io.Writer) (bool, error),Create(name string, tmplType ExtTemplateType) error,EnableDryRunMode(),UpdateDir(name string) string - Purpose: Full lifecycle management for
gh-*extension executables: discovery, installation from GitHub repos or local paths, upgrading, removal, and dispatch (running) the extension with forwarded I/O. TheDispatchmethod is the critical hotpath called at startup for every extension-registered command. - Implementations:
pkg/cmd/extension.Manager(concrete); moq-generated mock - Design quality: Well-designed for the extension use case.
Dispatchcorrectly passesio.Reader/io.Writerrather than coupling toIOStreams, making it I/O-agnostic.EnableDryRunMode()is a mutator that affects behavior — arguably a test seam embedded in the production interface (similar toAuthConfig’s test-only setters), but at least it’s named for its purpose.
Extension#
- Package:
pkg/extensions - File:
pkg/extensions/extension.go:18 - Methods: 9 —
Name() string,Path() string,URL() string,CurrentVersion() string,LatestVersion() string,IsPinned() bool,UpdateAvailable() bool,IsBinary() bool,IsLocal() bool,Owner() string - Purpose: Read-only descriptor for an installed extension. All methods are pure observers (no mutation). Used by
ExtensionManager.List()to report extension state. - Implementations:
pkg/cmd/extension.Extension(private concrete type); moq-generated mock - Design quality: Good separation of read (
Extension) vs write (ExtensionManager) concerns. Slight ISP concern —Owner()is only relevant for GitHub-hosted extensions, not local ones, soIsLocal()andOwner()interact implicitly.
Prompter#
- Package:
internal/prompter - File:
internal/prompter/prompter.go:17 - Methods: 10 —
Select,MultiSelect,MultiSelectWithSearch,Input,Password,Confirm,AuthToken,ConfirmDeletion,InputHostname,MarkdownEditor - Purpose: Abstracts all interactive user prompting. The
New()factory selects one of three backends:surveyPrompter(AlecAivazis/survey, default),huhPrompter(charm.land/huh, experimental), oraccessiblePrompter(huh with accessibility mode enabled). This enables swapping the TUI toolkit without changing any command code. - Implementations:
surveyPrompter,huhPrompter,accessiblePrompter(all in same file); moq-generatedprompter_mock.go - Design quality: Moderately broad (10 methods), but the gh-specific methods (
AuthToken,ConfirmDeletion,InputHostname,MarkdownEditor) justify inclusion — they carry validation logic tied to gh’s domain. Notable: many command packages define their own narrowiprompterinterface (unexported, 2-4 methods) that is a structural subset ofPrompter, following Go’s consumer-defines-interface idiom while keeping the full contract in one place for injection.
Exporter#
- Package:
pkg/cmdutil - File:
pkg/cmdutil/json_flags.go:198 - Methods: 2 —
Fields() []string,Write(io *iostreams.IOStreams, data interface{}) error - Purpose: Output serialization contract for the
--json/--jq/--templateflags. Every command that supports structured output accepts anExporterand callsWriteto serialize the result. Decouples command business logic from JSON/template rendering. - Implementations:
jsonExporter(private, returned byNewJSONExporter()); tests typically injectnilto exercise the plain-text path - Design quality: Clean two-method interface.
Fields()allows commands to advertise which JSON fields they support (validated at flag parse time).Writeacceptsinterface{}— the concrete types implement an unexportedexportableinterface to control which fields are serialized, a pragmatic escape from Go’s lack of structural typing for serialization.
Detector (FeatureDetection)#
- Package:
internal/featuredetection - File:
internal/featuredetection/feature_detection.go:14 - Methods: 8 —
IssueFeatures,PullRequestFeatures,RepositoryFeatures,ProjectsV1,ProjectFeatures,SearchFeatures,ReleaseFeatures,ActionsFeatures— each returning a feature-set struct - Purpose: GHES compatibility gate. GitHub Enterprise Server lags behind github.com in API features. Before using advanced GraphQL fields, commands ask the
Detectorwhether the target host supports them. This avoids runtime 400 errors on older GHES versions. - Implementations:
featuredetection.detector(does parallel API probes usingerrgroup);featuredetection.DisabledDetector(all features on, for github.com); stub for tests - Design quality: Returns structs of boolean flags rather than one method per flag — a practical choice that avoids interface explosion as new features are added. The pattern mirrors feature flags but bounded to host capability rather than progressive rollout.
Searcher#
- Package:
pkg/search - File:
pkg/search/searcher.go:29 - Methods: 5 —
Code(Query) (CodeResult, error),Commits(Query) (CommitsResult, error),Repositories(Query) (RepositoriesResult, error),Issues(Query) (IssuesResult, error),URL(Query) string - Purpose: Abstracts GitHub Search API calls for the
gh searchcommand family.URLgenerates a browser URL for the same query — useful for the--webflag. - Implementations:
search.searcher(concrete, HTTP client wrapper); moq-generatedsearcher_mock.go - Design quality: Well-segregated. Five methods map cleanly to the five search types gh exposes. The
URLmethod is a mild ISP concern (it’s a URL builder, not a search operation) but coupling it here avoids an extra abstraction.
Browser#
- Package:
internal/browser - File:
internal/browser/browser.go:9 - Methods: 1 —
Browse(string) error - Purpose: Minimal abstraction over launching a URL in the user’s default browser. Defined in
internal/browser, re-exported viaFactory.Browser. - Implementations:
cli/go-gh/v2/pkg/browser.Browser(delegates toopen/xdg-open); stub in tests - Design quality: Textbook single-method interface following Go idiom. Satisfies the browser contract for both
--webflags and OAuth browser flows.
Interface patterns#
- Size distribution: Highly bimodal. Core domain interfaces (
Config17,AuthConfig13+3,Prompter10) are broad — they must cover the full contract. Service/utility interfaces (Browser1,ghrepo.Interface3,Migration3,AliasConfig4,Exporter2) are narrow and precise. The average per interface is ~6 methods, but the distribution is bimodal, not gradual. - Embedding: Not used in the public interface catalog.
ConfigcomposesAuthConfigandAliasConfigvia method returns (factory methods) rather than embedding — a deliberate choice that maintains clear ownership. - Implicit satisfaction: Mixed strategy. The core domain interfaces (
Config,AuthConfig,ghrepo.Interface) are defined by the package that owns the concept (provider-defined) and implemented by a concrete package. Per-commandiprompter,gitClient,browserinterfaces throughoutpkg/cmd/*are defined in the consuming package (consumer-defined), matching Go best practice. Both patterns coexist deliberately. - stdlib interfaces used:
io.Reader,io.Writerappear inExtensionManager.DispatchandExporter.Write. No direct use offmt.Stringer,sort.Interface, orio.ReadCloserat the interface boundary level. - Mock generation: Interfaces with
//go:generate moq -rm ...directives:Config,Migration,AuthConfig,Extension,ExtensionManager,Prompter,Searcher. This marks the team’s “serious” interfaces — the ones that need injection in unit tests. One-off command-level interfaces (theiprompterpattern) are usually satisfied by hand-written stubs in_test.gofiles.
Key abstractions#
ghrepo.Interface(internal/ghrepo/repo.go) — The most widely-used interface in the codebase. Every command that touches a repo passes this type. Its 3-method minimal shape and host-awareness are the architectural linchpin enabling GHES support without conditional code in commands.gh.Config+gh.AuthConfig(internal/gh/gh.go) — The domain contract layer. Separating config from its implementation allows the config subsystem to evolve (e.g., theMultiAccountmigration) without touching command code. The acknowledged design smell of test-only methods onAuthConfigis worth noting for the book as a real-world tradeoff.extensions.ExtensionManager(pkg/extensions/extension.go) — The extensibility contract. TheDispatchmethod’s signature (args []string, stdin io.Reader, stdout, stderr io.Writer) is a deliberate design: it matchesos.Execsemantics exactly, making extensions feel like first-class subcommands without coupling to gh internals.prompter.Prompter(internal/prompter/prompter.go) — The I/O interaction boundary. Its real architectural significance is the factory function that selects among three TUI backends (survey, huh, accessible) at runtime. The consumer-sideiprompterpattern (narrow structural subsets defined per command) demonstrates Go’s implicit interface satisfaction in practice.cmdutil.Exporter(pkg/cmdutil/json_flags.go) — The structured output contract. Every command wiring the--jsonflag receives anExporterand callsWrite. This simple 2-method interface enables all ofgh’s JSON, JQ filtering, and Go template output from a single injection point.
Interface-driven extensibility#
gh uses interfaces for three distinct extensibility mechanisms:
Extension system (
ExtensionManager): Third-party binaries installed asgh-*are registered as Cobra subcommands viaExtensionManager.List()and dispatched viaExtensionManager.Dispatch(). The interface boundary means the core application never knows the implementation details of any particular extension.Config migration (
Migration): TheConfig.Migrate(Migration)pattern allows new config schema versions to be added without modifying the config infrastructure. New migrations implementMigrationand are passed toMigrate()at startup — a clean strategy/plugin pattern for schema evolution.Feature gating (
Detector): Commands queryDetectorto determine which API features are available on the target host. TheDisabledDetector(all features on) vs realdetector(API probes) pair is an interface-based adapter that decouples host capability from command logic. New GHES compatibility flags are added to the returned structs without changing the interface itself.
The per-command iprompter, gitClient, and browser local interfaces demonstrate Go’s structural typing used as narrow seams for unit testing — not for runtime extensibility. These are defined by the consumer purely to make mock injection possible without importing the full concrete type.