Skip to content

Commit af812e2

Browse files
author
Nate Smith
authored
Merge pull request cli#4373 from cli/ext-bin-upgrade
binary extensions list & upgrade
2 parents 425bc64 + 7efd06b commit af812e2

File tree

8 files changed

+337
-97
lines changed

8 files changed

+337
-97
lines changed

pkg/cmd/extension/command.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
132132
if len(args) > 0 {
133133
name = normalizeExtensionSelector(args[0])
134134
}
135-
return m.Upgrade(name, flagForce, io.Out, io.ErrOut)
135+
return m.Upgrade(name, flagForce)
136136
},
137137
}
138138
cmd.Flags().BoolVar(&flagAll, "all", false, "Upgrade all extensions")

pkg/cmd/extension/command_test.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package extension
22

33
import (
4-
"io"
54
"io/ioutil"
65
"net/http"
76
"os"
@@ -94,7 +93,7 @@ func TestNewCmdExtension(t *testing.T) {
9493
name: "upgrade an extension",
9594
args: []string{"upgrade", "hello"},
9695
managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
97-
em.UpgradeFunc = func(name string, force bool, out, errOut io.Writer) error {
96+
em.UpgradeFunc = func(name string, force bool) error {
9897
return nil
9998
}
10099
return func(t *testing.T) {
@@ -108,7 +107,7 @@ func TestNewCmdExtension(t *testing.T) {
108107
name: "upgrade an extension gh-prefix",
109108
args: []string{"upgrade", "gh-hello"},
110109
managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
111-
em.UpgradeFunc = func(name string, force bool, out, errOut io.Writer) error {
110+
em.UpgradeFunc = func(name string, force bool) error {
112111
return nil
113112
}
114113
return func(t *testing.T) {
@@ -122,7 +121,7 @@ func TestNewCmdExtension(t *testing.T) {
122121
name: "upgrade an extension full name",
123122
args: []string{"upgrade", "monalisa/gh-hello"},
124123
managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
125-
em.UpgradeFunc = func(name string, force bool, out, errOut io.Writer) error {
124+
em.UpgradeFunc = func(name string, force bool) error {
126125
return nil
127126
}
128127
return func(t *testing.T) {
@@ -136,7 +135,7 @@ func TestNewCmdExtension(t *testing.T) {
136135
name: "upgrade all",
137136
args: []string{"upgrade", "--all"},
138137
managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
139-
em.UpgradeFunc = func(name string, force bool, out, errOut io.Writer) error {
138+
em.UpgradeFunc = func(name string, force bool) error {
140139
return nil
141140
}
142141
return func(t *testing.T) {

pkg/cmd/extension/extension.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"strings"
66
)
77

8+
const manifestName = "manifest.yml"
9+
810
type Extension struct {
911
path string
1012
url string

pkg/cmd/extension/http.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func downloadAsset(httpClient *http.Client, asset releaseAsset, destPath string)
7070
return api.HandleHTTPError(resp)
7171
}
7272

73-
f, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0755)
73+
f, err := os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
7474
if err != nil {
7575
return err
7676
}

pkg/cmd/extension/manager.go

Lines changed: 141 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"io"
8+
"io/fs"
89
"io/ioutil"
910
"net/http"
1011
"os"
@@ -116,36 +117,96 @@ func (m *Manager) list(includeMetadata bool) ([]extensions.Extension, error) {
116117
if !strings.HasPrefix(f.Name(), "gh-") {
117118
continue
118119
}
119-
var remoteUrl string
120-
updateAvailable := false
121-
isLocal := false
122-
exePath := filepath.Join(dir, f.Name(), f.Name())
123-
if f.IsDir() {
124-
if includeMetadata {
125-
remoteUrl = m.getRemoteUrl(f.Name())
126-
updateAvailable = m.checkUpdateAvailable(f.Name())
127-
}
128-
} else {
129-
isLocal = true
130-
if !isSymlink(f.Mode()) {
131-
// if this is a regular file, its contents is the local directory of the extension
132-
p, err := readPathFromFile(filepath.Join(dir, f.Name()))
133-
if err != nil {
134-
return nil, err
135-
}
136-
exePath = filepath.Join(p, f.Name())
137-
}
120+
ext, err := m.parseExtensionDir(f, includeMetadata)
121+
if err != nil {
122+
return nil, err
138123
}
139-
results = append(results, &Extension{
140-
path: exePath,
141-
url: remoteUrl,
142-
isLocal: isLocal,
143-
updateAvailable: updateAvailable,
144-
})
124+
results = append(results, ext)
145125
}
126+
146127
return results, nil
147128
}
148129

130+
func (m *Manager) parseExtensionDir(fi fs.FileInfo, includeMetadata bool) (*Extension, error) {
131+
id := m.installDir()
132+
if _, err := os.Stat(filepath.Join(id, fi.Name(), manifestName)); err == nil {
133+
return m.parseBinaryExtensionDir(fi, includeMetadata)
134+
}
135+
136+
return m.parseGitExtensionDir(fi, includeMetadata)
137+
}
138+
139+
func (m *Manager) parseBinaryExtensionDir(fi fs.FileInfo, includeMetadata bool) (*Extension, error) {
140+
id := m.installDir()
141+
exePath := filepath.Join(id, fi.Name(), fi.Name())
142+
manifestPath := filepath.Join(id, fi.Name(), manifestName)
143+
manifest, err := os.ReadFile(manifestPath)
144+
if err != nil {
145+
return nil, fmt.Errorf("could not open %s for reading: %w", manifestPath, err)
146+
}
147+
148+
var bm binManifest
149+
err = yaml.Unmarshal(manifest, &bm)
150+
if err != nil {
151+
return nil, fmt.Errorf("could not parse %s: %w", manifestPath, err)
152+
}
153+
154+
repo := ghrepo.NewWithHost(bm.Owner, bm.Name, bm.Host)
155+
156+
var remoteURL string
157+
var updateAvailable bool
158+
159+
if includeMetadata {
160+
remoteURL = ghrepo.GenerateRepoURL(repo, "")
161+
var r *release
162+
r, err = fetchLatestRelease(m.client, repo)
163+
if err != nil {
164+
return nil, fmt.Errorf("failed to get release info for %s: %w", ghrepo.FullName(repo), err)
165+
}
166+
if bm.Tag != r.Tag {
167+
updateAvailable = true
168+
}
169+
}
170+
171+
return &Extension{
172+
path: exePath,
173+
url: remoteURL,
174+
updateAvailable: updateAvailable,
175+
}, nil
176+
}
177+
178+
func (m *Manager) parseGitExtensionDir(fi fs.FileInfo, includeMetadata bool) (*Extension, error) {
179+
// TODO untangle local from this since local might be binary or git
180+
id := m.installDir()
181+
var remoteUrl string
182+
updateAvailable := false
183+
isLocal := false
184+
exePath := filepath.Join(id, fi.Name(), fi.Name())
185+
if fi.IsDir() {
186+
if includeMetadata {
187+
remoteUrl = m.getRemoteUrl(fi.Name())
188+
updateAvailable = m.checkUpdateAvailable(fi.Name())
189+
}
190+
} else {
191+
isLocal = true
192+
if !isSymlink(fi.Mode()) {
193+
// if this is a regular file, its contents is the local directory of the extension
194+
p, err := readPathFromFile(filepath.Join(id, fi.Name()))
195+
if err != nil {
196+
return nil, err
197+
}
198+
exePath = filepath.Join(p, fi.Name())
199+
}
200+
}
201+
202+
return &Extension{
203+
path: exePath,
204+
url: remoteUrl,
205+
isLocal: isLocal,
206+
updateAvailable: updateAvailable,
207+
}, nil
208+
}
209+
149210
func (m *Manager) getRemoteUrl(extension string) string {
150211
gitExe, err := m.lookPath("git")
151212
if err != nil {
@@ -273,7 +334,7 @@ func (m *Manager) installBin(repo ghrepo.Interface) error {
273334
return fmt.Errorf("failed to serialize manifest: %w", err)
274335
}
275336

276-
manifestPath := filepath.Join(targetDir, "manifest.yml")
337+
manifestPath := filepath.Join(targetDir, manifestName)
277338

278339
f, err := os.OpenFile(manifestPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
279340
if err != nil {
@@ -306,7 +367,7 @@ func (m *Manager) installGit(cloneURL string, stdout, stderr io.Writer) error {
306367

307368
var localExtensionUpgradeError = errors.New("local extensions can not be upgraded")
308369

309-
func (m *Manager) Upgrade(name string, force bool, stdout, stderr io.Writer) error {
370+
func (m *Manager) Upgrade(name string, force bool) error {
310371
exe, err := m.lookPath("git")
311372
if err != nil {
312373
return err
@@ -320,41 +381,82 @@ func (m *Manager) Upgrade(name string, force bool, stdout, stderr io.Writer) err
320381
someUpgraded := false
321382
for _, f := range exts {
322383
if name == "" {
323-
fmt.Fprintf(stdout, "[%s]: ", f.Name())
384+
fmt.Fprintf(m.io.Out, "[%s]: ", f.Name())
324385
} else if f.Name() != name {
325386
continue
326387
}
327388

328389
if f.IsLocal() {
329390
if name == "" {
330-
fmt.Fprintf(stdout, "%s\n", localExtensionUpgradeError)
391+
fmt.Fprintf(m.io.Out, "%s\n", localExtensionUpgradeError)
331392
} else {
332393
err = localExtensionUpgradeError
333394
}
334395
continue
335396
}
336397

337-
var cmds []*exec.Cmd
338-
dir := filepath.Dir(f.Path())
339-
if force {
340-
fetchCmd := m.newCommand(exe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "fetch", "origin", "HEAD")
341-
resetCmd := m.newCommand(exe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "reset", "--hard", "origin/HEAD")
342-
cmds = []*exec.Cmd{fetchCmd, resetCmd}
343-
} else {
344-
pullCmd := m.newCommand(exe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "pull", "--ff-only")
345-
cmds = []*exec.Cmd{pullCmd}
398+
binManifestPath := filepath.Join(filepath.Dir(f.Path()), manifestName)
399+
if _, e := os.Stat(binManifestPath); e == nil {
400+
err = m.upgradeBin(f)
401+
someUpgraded = true
402+
continue
346403
}
347-
if e := runCmds(cmds, stdout, stderr); e != nil {
404+
405+
if e := m.upgradeGit(f, exe, force); e != nil {
348406
err = e
349407
}
350408
someUpgraded = true
351409
}
410+
352411
if err == nil && !someUpgraded {
353412
err = fmt.Errorf("no extension matched %q", name)
354413
}
414+
355415
return err
356416
}
357417

418+
func (m *Manager) upgradeGit(ext extensions.Extension, exe string, force bool) error {
419+
var cmds []*exec.Cmd
420+
dir := filepath.Dir(ext.Path())
421+
if force {
422+
fetchCmd := m.newCommand(exe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "fetch", "origin", "HEAD")
423+
resetCmd := m.newCommand(exe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "reset", "--hard", "origin/HEAD")
424+
cmds = []*exec.Cmd{fetchCmd, resetCmd}
425+
} else {
426+
pullCmd := m.newCommand(exe, "-C", dir, "--git-dir="+filepath.Join(dir, ".git"), "pull", "--ff-only")
427+
cmds = []*exec.Cmd{pullCmd}
428+
}
429+
430+
return runCmds(cmds, m.io.Out, m.io.ErrOut)
431+
}
432+
433+
func (m *Manager) upgradeBin(ext extensions.Extension) error {
434+
manifestPath := filepath.Join(filepath.Dir(ext.Path()), manifestName)
435+
manifest, err := os.ReadFile(manifestPath)
436+
if err != nil {
437+
return fmt.Errorf("could not open %s for reading: %w", manifestPath, err)
438+
}
439+
440+
var bm binManifest
441+
err = yaml.Unmarshal(manifest, &bm)
442+
if err != nil {
443+
return fmt.Errorf("could not parse %s: %w", manifestPath, err)
444+
}
445+
repo := ghrepo.NewWithHost(bm.Owner, bm.Name, bm.Host)
446+
var r *release
447+
448+
r, err = fetchLatestRelease(m.client, repo)
449+
if err != nil {
450+
return fmt.Errorf("failed to get release info for %s: %w", ghrepo.FullName(repo), err)
451+
}
452+
453+
if bm.Tag == r.Tag {
454+
return nil
455+
}
456+
457+
return m.installBin(repo)
458+
}
459+
358460
func (m *Manager) Remove(name string) error {
359461
targetDir := filepath.Join(m.installDir(), "gh-"+name)
360462
if _, err := os.Lstat(targetDir); os.IsNotExist(err) {

0 commit comments

Comments
 (0)