X Tutup
Skip to content

Commit c1e5934

Browse files
author
Nate Smith
authored
Merge pull request cli#5272 from meiji163/pin-ext
2 parents 52576c2 + 45845bc commit c1e5934

File tree

9 files changed

+466
-61
lines changed

9 files changed

+466
-61
lines changed

pkg/cmd/extension/command.go

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,13 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
6565
if !c.IsBinary() && len(version) > 8 {
6666
version = version[:8]
6767
}
68-
t.AddField(version, nil, nil)
68+
69+
if c.IsPinned() {
70+
t.AddField(version, nil, cs.Cyan)
71+
} else {
72+
t.AddField(version, nil, nil)
73+
}
74+
6975
var updateAvailable string
7076
if c.UpdateAvailable() {
7177
updateAvailable = "Upgrade available"
@@ -76,10 +82,12 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
7682
return t.Render()
7783
},
7884
},
79-
&cobra.Command{
80-
Use: "install <repository>",
81-
Short: "Install a gh extension from a repository",
82-
Long: heredoc.Doc(`
85+
func() *cobra.Command {
86+
var pinFlag string
87+
cmd := &cobra.Command{
88+
Use: "install <repository>",
89+
Short: "Install a gh extension from a repository",
90+
Long: heredoc.Doc(`
8391
Install a GitHub repository locally as a GitHub CLI extension.
8492
8593
The repository argument can be specified in "owner/repo" format as well as a full URL.
@@ -90,41 +98,57 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
9098
9199
See the list of available extensions at <https://github.com/topics/gh-extension>.
92100
`),
93-
Example: heredoc.Doc(`
101+
Example: heredoc.Doc(`
94102
$ gh extension install owner/gh-extension
95103
$ gh extension install https://git.example.com/owner/gh-extension
96104
$ gh extension install .
97105
`),
98-
Args: cmdutil.MinimumArgs(1, "must specify a repository to install from"),
99-
RunE: func(cmd *cobra.Command, args []string) error {
100-
if args[0] == "." {
101-
wd, err := os.Getwd()
106+
Args: cmdutil.MinimumArgs(1, "must specify a repository to install from"),
107+
RunE: func(cmd *cobra.Command, args []string) error {
108+
if args[0] == "." {
109+
if pinFlag != "" {
110+
return fmt.Errorf("local extensions cannot be pinned")
111+
}
112+
wd, err := os.Getwd()
113+
if err != nil {
114+
return err
115+
}
116+
return m.InstallLocal(wd)
117+
}
118+
119+
repo, err := ghrepo.FromFullName(args[0])
102120
if err != nil {
103121
return err
104122
}
105-
return m.InstallLocal(wd)
106-
}
107-
108-
repo, err := ghrepo.FromFullName(args[0])
109-
if err != nil {
110-
return err
111-
}
112-
113-
if err := checkValidExtension(cmd.Root(), m, repo.RepoName()); err != nil {
114-
return err
115-
}
116123

117-
if err := m.Install(repo); err != nil {
118-
return err
119-
}
124+
if err := checkValidExtension(cmd.Root(), m, repo.RepoName()); err != nil {
125+
return err
126+
}
120127

121-
if io.IsStdoutTTY() {
122128
cs := io.ColorScheme()
123-
fmt.Fprintf(io.Out, "%s Installed extension %s\n", cs.SuccessIcon(), args[0])
124-
}
125-
return nil
126-
},
127-
},
129+
if err := m.Install(repo, pinFlag); err != nil {
130+
if errors.Is(err, releaseNotFoundErr) {
131+
return fmt.Errorf("%s Could not find a release of %s for %s",
132+
cs.FailureIcon(), args[0], cs.Cyan(pinFlag))
133+
} else if errors.Is(err, commitNotFoundErr) {
134+
return fmt.Errorf("%s %s does not exist in %s",
135+
cs.FailureIcon(), cs.Cyan(pinFlag), args[0])
136+
}
137+
return err
138+
}
139+
140+
if io.IsStdoutTTY() {
141+
fmt.Fprintf(io.Out, "%s Installed extension %s\n", cs.SuccessIcon(), args[0])
142+
if pinFlag != "" {
143+
fmt.Fprintf(io.Out, "%s Pinned extension at %s\n", cs.SuccessIcon(), cs.Cyan(pinFlag))
144+
}
145+
}
146+
return nil
147+
},
148+
}
149+
cmd.Flags().StringVar(&pinFlag, "pin", "", "pin extension to a release tag or commit ref")
150+
return cmd
151+
}(),
128152
func() *cobra.Command {
129153
var flagAll bool
130154
var flagForce bool

pkg/cmd/extension/command_test.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func TestNewCmdExtension(t *testing.T) {
4444
em.ListFunc = func(bool) []extensions.Extension {
4545
return []extensions.Extension{}
4646
}
47-
em.InstallFunc = func(_ ghrepo.Interface) error {
47+
em.InstallFunc = func(_ ghrepo.Interface, _ string) error {
4848
return nil
4949
}
5050
return func(t *testing.T) {
@@ -86,6 +86,13 @@ func TestNewCmdExtension(t *testing.T) {
8686
}
8787
},
8888
},
89+
{
90+
name: "install local extension with pin",
91+
args: []string{"install", ".", "--pin", "v1.0.0"},
92+
wantErr: true,
93+
errMsg: "local extensions cannot be pinned",
94+
isTTY: true,
95+
},
8996
{
9097
name: "upgrade argument error",
9198
args: []string{"upgrade"},

pkg/cmd/extension/extension.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type Extension struct {
1818
path string
1919
url string
2020
isLocal bool
21+
isPinned bool
2122
currentVersion string
2223
latestVersion string
2324
kind ExtensionKind
@@ -43,8 +44,13 @@ func (e *Extension) CurrentVersion() string {
4344
return e.currentVersion
4445
}
4546

47+
func (e *Extension) IsPinned() bool {
48+
return e.isPinned
49+
}
50+
4651
func (e *Extension) UpdateAvailable() bool {
47-
if e.isLocal ||
52+
if e.isPinned ||
53+
e.isLocal ||
4854
e.currentVersion == "" ||
4955
e.latestVersion == "" ||
5056
e.currentVersion == e.latestVersion {

pkg/cmd/extension/http.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package extension
22

33
import (
44
"encoding/json"
5+
"errors"
56
"fmt"
67
"io"
78
"io/ioutil"
@@ -80,6 +81,9 @@ func downloadAsset(httpClient *http.Client, asset releaseAsset, destPath string)
8081
return err
8182
}
8283

84+
var releaseNotFoundErr = errors.New("release not found")
85+
var commitNotFoundErr = errors.New("commit not found")
86+
8387
// fetchLatestRelease finds the latest published release for a repository.
8488
func fetchLatestRelease(httpClient *http.Client, baseRepo ghrepo.Interface) (*release, error) {
8589
path := fmt.Sprintf("repos/%s/%s/releases/latest", baseRepo.RepoOwner(), baseRepo.RepoName())
@@ -112,3 +116,71 @@ func fetchLatestRelease(httpClient *http.Client, baseRepo ghrepo.Interface) (*re
112116

113117
return &r, nil
114118
}
119+
120+
// fetchReleaseFromTag finds release by tag name for a repository
121+
func fetchReleaseFromTag(httpClient *http.Client, baseRepo ghrepo.Interface, tagName string) (*release, error) {
122+
fullRepoName := fmt.Sprintf("%s/%s", baseRepo.RepoOwner(), baseRepo.RepoName())
123+
path := fmt.Sprintf("repos/%s/releases/tags/%s", fullRepoName, tagName)
124+
url := ghinstance.RESTPrefix(baseRepo.RepoHost()) + path
125+
req, err := http.NewRequest("GET", url, nil)
126+
if err != nil {
127+
return nil, err
128+
}
129+
130+
resp, err := httpClient.Do(req)
131+
if err != nil {
132+
return nil, err
133+
}
134+
135+
defer resp.Body.Close()
136+
if resp.StatusCode == 404 {
137+
return nil, releaseNotFoundErr
138+
}
139+
if resp.StatusCode > 299 {
140+
return nil, api.HandleHTTPError(resp)
141+
}
142+
143+
b, err := ioutil.ReadAll(resp.Body)
144+
if err != nil {
145+
return nil, err
146+
}
147+
148+
var r release
149+
err = json.Unmarshal(b, &r)
150+
if err != nil {
151+
return nil, err
152+
}
153+
154+
return &r, nil
155+
}
156+
157+
// fetchCommitSHA finds full commit SHA from a target ref in a repo
158+
func fetchCommitSHA(httpClient *http.Client, baseRepo ghrepo.Interface, targetRef string) (string, error) {
159+
path := fmt.Sprintf("repos/%s/%s/commits/%s", baseRepo.RepoOwner(), baseRepo.RepoName(), targetRef)
160+
url := ghinstance.RESTPrefix(baseRepo.RepoHost()) + path
161+
req, err := http.NewRequest("GET", url, nil)
162+
if err != nil {
163+
return "", err
164+
}
165+
166+
req.Header.Set("Accept", "application/vnd.github.VERSION.sha")
167+
resp, err := httpClient.Do(req)
168+
if err != nil {
169+
return "", err
170+
}
171+
172+
defer resp.Body.Close()
173+
if resp.StatusCode == 422 {
174+
return "", commitNotFoundErr
175+
}
176+
if resp.StatusCode > 299 {
177+
return "", api.HandleHTTPError(resp)
178+
}
179+
180+
body, err := ioutil.ReadAll(resp.Body)
181+
if err != nil {
182+
return "", err
183+
}
184+
185+
return string(body), nil
186+
}

0 commit comments

Comments
 (0)
X Tutup