Add godoc comments to exported symbols in pkg/cmd/extension

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Kynan Ware 2026-03-04 15:58:15 -07:00
parent d6ab1b044a
commit 520482c143
7 changed files with 68 additions and 2 deletions

View file

@ -25,6 +25,7 @@ import (
const pagingOffset = 24
// ExtBrowseOpts holds the options for the extension browse command.
type ExtBrowseOpts struct {
Cmd *cobra.Command
Browser ibrowser
@ -63,6 +64,7 @@ type extEntry struct {
description string
}
// Title returns the display title for the extension entry including status indicators.
func (e extEntry) Title() string {
var installed string
var official string
@ -78,6 +80,7 @@ func (e extEntry) Title() string {
return fmt.Sprintf("%s%s%s", e.FullName, official, installed)
}
// Description returns the extension description or a default placeholder.
func (e extEntry) Description() string {
if e.description == "" {
return "no description provided"
@ -103,9 +106,14 @@ type wGroup interface {
type fakeGroup struct{}
// Add is a no-op implementation of the wGroup interface.
func (w *fakeGroup) Add(int) {}
func (w *fakeGroup) Done() {}
func (w *fakeGroup) Wait() {}
// Done is a no-op implementation of the wGroup interface.
func (w *fakeGroup) Done() {}
// Wait is a no-op implementation of the wGroup interface.
func (w *fakeGroup) Wait() {}
func newExtList(opts ExtBrowseOpts, ui uiRegistry, extEntries []extEntry) *extList {
ui.List.SetTitleColor(tcell.ColorWhite)
@ -219,10 +227,12 @@ func (el *extList) toggleSelected(verb string) {
}
}
// InstallSelected installs the currently highlighted extension.
func (el *extList) InstallSelected() {
el.toggleSelected("install")
}
// RemoveSelected removes the currently highlighted extension.
func (el *extList) RemoveSelected() {
el.toggleSelected("remove")
}
@ -233,15 +243,18 @@ func (el *extList) toggleInstalled(ix int) {
el.extEntries[ix] = ee
}
// Focus sets the application focus to the extension list.
func (el *extList) Focus() {
el.app.SetFocus(el.ui.List)
}
// Refresh resets the list and reapplies the current filter.
func (el *extList) Refresh() {
el.Reset()
el.Filter(el.filter)
}
// Reset clears the list and repopulates it with all extension entries.
func (el *extList) Reset() {
el.ui.List.Clear()
for _, ee := range el.extEntries {
@ -249,10 +262,12 @@ func (el *extList) Reset() {
}
}
// PageDown moves the list selection down by one page.
func (el *extList) PageDown() {
el.ui.List.SetCurrentItem(el.ui.List.GetCurrentItem() + pagingOffset)
}
// PageUp moves the list selection up by one page.
func (el *extList) PageUp() {
i := el.ui.List.GetCurrentItem() - pagingOffset
if i < 0 {
@ -261,10 +276,12 @@ func (el *extList) PageUp() {
el.ui.List.SetCurrentItem(i)
}
// ScrollDown moves the list selection down by one item.
func (el *extList) ScrollDown() {
el.ui.List.SetCurrentItem(el.ui.List.GetCurrentItem() + 1)
}
// ScrollUp moves the list selection up by one item.
func (el *extList) ScrollUp() {
i := el.ui.List.GetCurrentItem() - 1
if i < 0 {
@ -273,6 +290,7 @@ func (el *extList) ScrollUp() {
el.ui.List.SetCurrentItem(i)
}
// FindSelected returns the currently selected extension entry and its index.
func (el *extList) FindSelected() (extEntry, int) {
if el.ui.List.GetItemCount() == 0 {
return extEntry{}, -1
@ -286,6 +304,7 @@ func (el *extList) FindSelected() (extEntry, int) {
return extEntry{}, -1
}
// Filter narrows the displayed list to entries matching the given text.
func (el *extList) Filter(text string) {
el.filter = text
if text == "" {
@ -377,6 +396,7 @@ func getExtensions(opts ExtBrowseOpts) ([]extEntry, error) {
return extEntries, nil
}
// ExtBrowse launches the interactive TUI for browsing and managing extensions.
func ExtBrowse(opts ExtBrowseOpts) error {
if opts.Debug {
f, err := os.CreateTemp("", "extBrowse-*.txt")

View file

@ -20,6 +20,7 @@ func newReadmeGetter(client *http.Client, cacheTTL time.Duration) *readmeGetter
}
}
// Get fetches the README content for the given repository full name.
func (g *readmeGetter) Get(repoFullName string) (string, error) {
repo, err := ghrepo.FromFullName(repoFullName)
if err != nil {

View file

@ -25,6 +25,7 @@ import (
var alreadyInstalledError = errors.New("alreadyInstalledError")
// NewCmdExtension creates the cobra command for managing gh extensions.
func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
m := f.ExtensionManager
io := f.IOStreams

View file

@ -16,14 +16,19 @@ import (
const manifestName = "manifest.yml"
// ExtensionKind indicates the type of a gh CLI extension.
type ExtensionKind int
const (
// GitKind represents a git-based extension.
GitKind ExtensionKind = iota
// BinaryKind represents a precompiled binary extension.
BinaryKind
// LocalKind represents a locally developed extension.
LocalKind
)
// Extension represents an installed gh CLI extension.
type Extension struct {
path string
kind ExtensionKind
@ -40,22 +45,27 @@ type Extension struct {
owner string
}
// Name returns the extension name without the "gh-" prefix.
func (e *Extension) Name() string {
return strings.TrimPrefix(filepath.Base(e.path), "gh-")
}
// Path returns the filesystem path to the extension executable.
func (e *Extension) Path() string {
return e.path
}
// IsLocal returns true if the extension is a locally developed extension.
func (e *Extension) IsLocal() bool {
return e.kind == LocalKind
}
// IsBinary returns true if the extension is a precompiled binary.
func (e *Extension) IsBinary() bool {
return e.kind == BinaryKind
}
// URL returns the repository URL of the extension.
func (e *Extension) URL() string {
e.mu.RLock()
if e.url != "" {
@ -85,6 +95,7 @@ func (e *Extension) URL() string {
return e.url
}
// CurrentVersion returns the currently installed version of the extension.
func (e *Extension) CurrentVersion() string {
e.mu.RLock()
if e.currentVersion != "" {
@ -113,6 +124,7 @@ func (e *Extension) CurrentVersion() string {
return e.currentVersion
}
// LatestVersion returns the latest available version of the extension.
func (e *Extension) LatestVersion() string {
e.mu.RLock()
if e.latestVersion != "" {
@ -147,6 +159,7 @@ func (e *Extension) LatestVersion() string {
return e.latestVersion
}
// IsPinned returns true if the extension is pinned to a specific version.
func (e *Extension) IsPinned() bool {
e.mu.RLock()
if e.isPinned != nil {
@ -179,6 +192,7 @@ func (e *Extension) IsPinned() bool {
return *e.isPinned
}
// Owner returns the GitHub owner of the extension repository.
func (e *Extension) Owner() string {
e.mu.RLock()
if e.owner != "" {
@ -211,6 +225,7 @@ func (e *Extension) Owner() string {
return e.owner
}
// UpdateAvailable returns true if a newer version of the extension exists.
func (e *Extension) UpdateAvailable() bool {
if e.IsLocal() ||
e.CurrentVersion() == "" ||

View file

@ -21,14 +21,17 @@ type gitExecuter struct {
client *git.Client
}
// CheckoutBranch checks out the specified branch in the repository.
func (g *gitExecuter) CheckoutBranch(branch string) error {
return g.client.CheckoutBranch(context.Background(), branch)
}
// Clone clones a repository from the given URL with optional arguments.
func (g *gitExecuter) Clone(cloneURL string, cloneArgs []string) (string, error) {
return g.client.Clone(context.Background(), cloneURL, cloneArgs)
}
// CommandOutput runs a git command and returns its output.
func (g *gitExecuter) CommandOutput(args []string) ([]byte, error) {
cmd, err := g.client.Command(context.Background(), args...)
if err != nil {
@ -37,24 +40,29 @@ func (g *gitExecuter) CommandOutput(args []string) ([]byte, error) {
return cmd.Output()
}
// Config retrieves a git configuration value by name.
func (g *gitExecuter) Config(name string) (string, error) {
return g.client.Config(context.Background(), name)
}
// Fetch fetches refs from the specified remote.
func (g *gitExecuter) Fetch(remote string, refspec string) error {
return g.client.Fetch(context.Background(), remote, refspec)
}
// ForRepo returns a new gitClient scoped to the given repository directory.
func (g *gitExecuter) ForRepo(repoDir string) gitClient {
gc := g.client.Copy()
gc.RepoDir = repoDir
return &gitExecuter{client: gc}
}
// Pull pulls changes from the specified remote and branch.
func (g *gitExecuter) Pull(remote, branch string) error {
return g.client.Pull(context.Background(), remote, branch)
}
// Remotes returns the set of configured git remotes.
func (g *gitExecuter) Remotes() (git.RemoteSet, error) {
return g.client.Remotes(context.Background())
}

View file

@ -30,17 +30,20 @@ import (
// ErrInitialCommitFailed indicates the initial commit when making a new extension failed.
var ErrInitialCommitFailed = errors.New("initial commit failed")
// ErrExtensionExecutableNotFound indicates that an installed extension has no executable.
type ErrExtensionExecutableNotFound struct {
Dir string
Name string
}
// Error returns a descriptive message about the missing executable.
func (e *ErrExtensionExecutableNotFound) Error() string {
return fmt.Sprintf("an extension has been installed but there is no executable: executable file named \"%s\" in %s is required to run the extension after install. Perhaps you need to build it?\n", e.Name, e.Dir)
}
const darwinAmd64 = "darwin-amd64"
// Manager handles installation, upgrade, and removal of gh CLI extensions.
type Manager struct {
dataDir func() string
updateDir func() string
@ -55,6 +58,7 @@ type Manager struct {
dryRunMode bool
}
// NewManager creates a new extension Manager with default settings.
func NewManager(ios *iostreams.IOStreams, gc *git.Client) *Manager {
return &Manager{
dataDir: config.DataDir,
@ -76,18 +80,22 @@ func NewManager(ios *iostreams.IOStreams, gc *git.Client) *Manager {
}
}
// SetConfig sets the configuration used by the Manager.
func (m *Manager) SetConfig(cfg gh.Config) {
m.config = cfg
}
// SetClient sets the HTTP client used by the Manager.
func (m *Manager) SetClient(client *http.Client) {
m.client = client
}
// EnableDryRunMode enables dry-run mode so that no changes are persisted.
func (m *Manager) EnableDryRunMode() {
m.dryRunMode = true
}
// Dispatch executes an installed extension by name with the given I/O streams.
func (m *Manager) Dispatch(args []string, stdin io.Reader, stdout, stderr io.Writer) (bool, error) {
if len(args) == 0 {
return false, errors.New("too few arguments in list")
@ -133,6 +141,7 @@ func (m *Manager) Dispatch(args []string, stdin io.Reader, stdout, stderr io.Wri
return true, externalCmd.Run()
}
// List returns all installed extensions.
func (m *Manager) List() []extensions.Extension {
exts, _ := m.list(false)
r := make([]extensions.Extension, len(exts))
@ -205,6 +214,7 @@ func (m *Manager) populateLatestVersions(exts []*Extension) {
wg.Wait()
}
// InstallLocal installs a local extension from the given directory.
func (m *Manager) InstallLocal(dir string) error {
name := filepath.Base(dir)
if err := m.cleanExtensionUpdateDir(name); err != nil {
@ -451,6 +461,7 @@ var localExtensionUpgradeError = errors.New("local extensions can not be upgrade
var upToDateError = errors.New("already up to date")
var noExtensionsInstalledError = errors.New("no extensions installed")
// Upgrade upgrades the named extension or all extensions if name is empty.
func (m *Manager) Upgrade(name string, force bool) error {
// Fetch metadata during list only when upgrading all extensions.
// This is a performance improvement so that we don't make a
@ -570,6 +581,7 @@ func (m *Manager) upgradeBinExtension(ext *Extension) error {
return m.installBin(repo, "")
}
// Remove uninstalls the named extension and cleans up its files.
func (m *Manager) Remove(name string) error {
name = normalizeExtension(name)
targetDir := filepath.Join(m.installDir(), name)
@ -609,6 +621,7 @@ var scriptTmpl string
//go:embed ext_tmpls/buildScript.sh
var buildScript []byte
// Create scaffolds a new extension project with the given name and template type.
func (m *Manager) Create(name string, tmplType extensions.ExtTemplateType) error {
if _, err := m.gitClient.CommandOutput([]string{"init", "--quiet", name}); err != nil {
return err

View file

@ -9,31 +9,37 @@ type mockGitClient struct {
mock.Mock
}
// CheckoutBranch mocks checking out a branch.
func (g *mockGitClient) CheckoutBranch(branch string) error {
args := g.Called(branch)
return args.Error(0)
}
// Clone mocks cloning a repository.
func (g *mockGitClient) Clone(cloneURL string, cloneArgs []string) (string, error) {
args := g.Called(cloneURL, cloneArgs)
return args.String(0), args.Error(1)
}
// CommandOutput mocks running a git command and returning its output.
func (g *mockGitClient) CommandOutput(commandArgs []string) ([]byte, error) {
args := g.Called(commandArgs)
return []byte(args.String(0)), args.Error(1)
}
// Config mocks retrieving a git configuration value.
func (g *mockGitClient) Config(name string) (string, error) {
args := g.Called(name)
return args.String(0), args.Error(1)
}
// Fetch mocks fetching refs from a remote.
func (g *mockGitClient) Fetch(remote string, refspec string) error {
args := g.Called(remote, refspec)
return args.Error(0)
}
// ForRepo mocks returning a gitClient scoped to a repository directory.
func (g *mockGitClient) ForRepo(repoDir string) gitClient {
args := g.Called(repoDir)
if v, ok := args.Get(0).(*mockGitClient); ok {
@ -42,11 +48,13 @@ func (g *mockGitClient) ForRepo(repoDir string) gitClient {
return nil
}
// Pull mocks pulling changes from a remote branch.
func (g *mockGitClient) Pull(remote, branch string) error {
args := g.Called(remote, branch)
return args.Error(0)
}
// Remotes mocks returning the set of configured git remotes.
func (g *mockGitClient) Remotes() (git.RemoteSet, error) {
args := g.Called()
return nil, args.Error(1)