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 AuthConfig and AliasConfig via accessor methods rather than embedding.
  • Implementations: internal/config (reads/writes ~/.config/gh/*.yml YAML via cli/go-gh/v2/pkg/config); ghmock.Config (generated mock via moq)
  • 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/Set primitives 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 by Config.Aliases().
  • Implementations: internal/config
  • Design quality: Tight, well-segregated. Four methods cover exactly the alias use case. Correctly separated from Config rather 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 invoking Do. 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 (Interface is unusual but the package name ghrepo provides 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. The Dispatch method 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. Dispatch correctly passes io.Reader/io.Writer rather than coupling to IOStreams, making it I/O-agnostic. EnableDryRunMode() is a mutator that affects behavior — arguably a test seam embedded in the production interface (similar to AuthConfig’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, so IsLocal() and Owner() 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), or accessiblePrompter (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-generated prompter_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 narrow iprompter interface (unexported, 2-4 methods) that is a structural subset of Prompter, 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 / --template flags. Every command that supports structured output accepts an Exporter and calls Write to serialize the result. Decouples command business logic from JSON/template rendering.
  • Implementations: jsonExporter (private, returned by NewJSONExporter()); tests typically inject nil to 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). Write accepts interface{} — the concrete types implement an unexported exportable interface 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 Detector whether the target host supports them. This avoids runtime 400 errors on older GHES versions.
  • Implementations: featuredetection.detector (does parallel API probes using errgroup); 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 search command family. URL generates a browser URL for the same query — useful for the --web flag.
  • Implementations: search.searcher (concrete, HTTP client wrapper); moq-generated searcher_mock.go
  • Design quality: Well-segregated. Five methods map cleanly to the five search types gh exposes. The URL method 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 via Factory.Browser.
  • Implementations: cli/go-gh/v2/pkg/browser.Browser (delegates to open/xdg-open); stub in tests
  • Design quality: Textbook single-method interface following Go idiom. Satisfies the browser contract for both --web flags and OAuth browser flows.

Interface patterns#

  • Size distribution: Highly bimodal. Core domain interfaces (Config 17, AuthConfig 13+3, Prompter 10) are broad — they must cover the full contract. Service/utility interfaces (Browser 1, ghrepo.Interface 3, Migration 3, AliasConfig 4, Exporter 2) 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. Config composes AuthConfig and AliasConfig via 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-command iprompter, gitClient, browser interfaces throughout pkg/cmd/* are defined in the consuming package (consumer-defined), matching Go best practice. Both patterns coexist deliberately.
  • stdlib interfaces used: io.Reader, io.Writer appear in ExtensionManager.Dispatch and Exporter.Write. No direct use of fmt.Stringer, sort.Interface, or io.ReadCloser at 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 (the iprompter pattern) are usually satisfied by hand-written stubs in _test.go files.

Key abstractions#

  1. 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.

  2. 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., the MultiAccount migration) without touching command code. The acknowledged design smell of test-only methods on AuthConfig is worth noting for the book as a real-world tradeoff.

  3. extensions.ExtensionManager (pkg/extensions/extension.go) — The extensibility contract. The Dispatch method’s signature (args []string, stdin io.Reader, stdout, stderr io.Writer) is a deliberate design: it matches os.Exec semantics exactly, making extensions feel like first-class subcommands without coupling to gh internals.

  4. 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-side iprompter pattern (narrow structural subsets defined per command) demonstrates Go’s implicit interface satisfaction in practice.

  5. cmdutil.Exporter (pkg/cmdutil/json_flags.go) — The structured output contract. Every command wiring the --json flag receives an Exporter and calls Write. This simple 2-method interface enables all of gh’s JSON, JQ filtering, and Go template output from a single injection point.


Interface-driven extensibility#

gh uses interfaces for three distinct extensibility mechanisms:

  1. Extension system (ExtensionManager): Third-party binaries installed as gh-* are registered as Cobra subcommands via ExtensionManager.List() and dispatched via ExtensionManager.Dispatch(). The interface boundary means the core application never knows the implementation details of any particular extension.

  2. Config migration (Migration): The Config.Migrate(Migration) pattern allows new config schema versions to be added without modifying the config infrastructure. New migrations implement Migration and are passed to Migrate() at startup — a clean strategy/plugin pattern for schema evolution.

  3. Feature gating (Detector): Commands query Detector to determine which API features are available on the target host. The DisabledDetector (all features on) vs real detector (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.