Skip to content

Commit 75aa7ec

Browse files
authored
Merge pull request #409 from rsteube/add-gh
added gh
2 parents dc347c4 + 9649ada commit 75aa7ec

File tree

119 files changed

+6786
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

119 files changed

+6786
-1
lines changed
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
package action
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"os"
8+
"strings"
9+
10+
"github.com/rsteube/carapace"
11+
"github.com/rsteube/carapace-bin/completers/gh_completer/cmd/action/config"
12+
"github.com/rsteube/carapace-bin/completers/gh_completer/cmd/action/ghrepo"
13+
"github.com/rsteube/carapace-bin/completers/gh_completer/cmd/action/git"
14+
"github.com/spf13/cobra"
15+
)
16+
17+
func ghExecutable() string {
18+
return "gh"
19+
}
20+
21+
func ActionConfigHosts() carapace.Action {
22+
return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
23+
if config, err := config.ParseDefaultConfig(); err != nil {
24+
return carapace.ActionMessage("failed to parse DefaultConfig: " + err.Error())
25+
} else {
26+
if hosts, err := config.Hosts(); err != nil {
27+
return carapace.ActionMessage("failed ot loadd hosts: " + err.Error())
28+
} else {
29+
return carapace.ActionValues(hosts...)
30+
}
31+
}
32+
})
33+
}
34+
35+
func ActionHttpMethods() carapace.Action {
36+
return carapace.ActionValuesDescribed(
37+
"POST", "submit an entity to the specified resource",
38+
"PATCH", "apply partial modifications to a resourc",
39+
"PUT", "replaces all current representations of the target resource with the request payload",
40+
"DELETE", "delete the specified resource",
41+
)
42+
}
43+
44+
func ApiV3Action(cmd *cobra.Command, query string, v interface{}, transform func() carapace.Action) carapace.Action {
45+
return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
46+
if repo, err := repoOverride(cmd); err != nil {
47+
return carapace.ActionMessage(err.Error())
48+
} else {
49+
return carapace.ActionExecCommand(ghExecutable(), "api", "--hostname", repo.RepoHost(), query)(func(output []byte) carapace.Action {
50+
if err := json.Unmarshal(output, &v); err != nil {
51+
return carapace.ActionMessage("failed to unmarshall response: " + err.Error())
52+
}
53+
return transform()
54+
})
55+
}
56+
})
57+
}
58+
59+
func GraphQlAction(cmd *cobra.Command, query string, v interface{}, transform func() carapace.Action) carapace.Action {
60+
return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
61+
params := make([]string, 0)
62+
if strings.Contains(query, "$owner") {
63+
params = append(params, "$owner: String!")
64+
}
65+
if strings.Contains(query, "$repo") {
66+
params = append(params, "$repo: String!")
67+
}
68+
queryParams := strings.Join(params, ",")
69+
if queryParams != "" {
70+
queryParams = "(" + queryParams + ")"
71+
}
72+
73+
if repo, err := repoOverride(cmd); err != nil {
74+
return carapace.ActionMessage(err.Error())
75+
} else {
76+
return carapace.ActionExecCommand(ghExecutable(), "api", "--hostname", repo.RepoHost(), "--header", "Accept: application/vnd.github.merge-info-preview+json", "graphql", "-F", "owner="+repo.RepoOwner(), "-F", "repo="+repo.RepoName(), "-f", fmt.Sprintf("query=query%v {%v}", queryParams, query))(func(output []byte) carapace.Action {
77+
if err := json.Unmarshal(output, &v); err != nil {
78+
return carapace.ActionMessage("failed to unmarshall response: " + err.Error())
79+
}
80+
return transform()
81+
})
82+
}
83+
})
84+
}
85+
86+
func repoOverride(cmd *cobra.Command) (ghrepo.Interface, error) {
87+
repoOverride := ""
88+
if flag := cmd.Flag("repo"); flag != nil {
89+
repoOverride = flag.Value.String()
90+
}
91+
if repoFromEnv := os.Getenv("GH_REPO"); repoOverride == "" && repoFromEnv != "" {
92+
repoOverride = repoFromEnv
93+
}
94+
if repoOverride != "" {
95+
splitted := strings.Split(repoOverride, "/")
96+
switch len(splitted) {
97+
case 1:
98+
return ghrepo.New(splitted[0], ""), nil // assume owner
99+
case 2:
100+
if strings.Contains(splitted[0], ".") {
101+
return ghrepo.NewWithHost(splitted[1], "", splitted[0]), nil
102+
} else {
103+
return ghrepo.New(splitted[0], splitted[1]), nil
104+
}
105+
default:
106+
return ghrepo.NewWithHost(splitted[1], splitted[2], splitted[0]), nil
107+
}
108+
} else {
109+
if remotes, err := git.Remotes(); err == nil {
110+
for _, remote := range remotes {
111+
if remote.Resolved == "base" {
112+
return ghrepo.FromURL(remote.FetchURL) // TODO ssh translator
113+
}
114+
}
115+
}
116+
}
117+
return ghrepo.New("", ""), nil
118+
}
119+
120+
func ActionAuthScopes() carapace.Action {
121+
return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
122+
return carapace.ActionValuesDescribed(
123+
"repo", "Grants full access to private and public repositories.",
124+
"repo:status", "Grants read/write access to public and private repository commit statuses.",
125+
"repo_deployment", "Grants access to deployment statuses for public and private repositories.",
126+
"public_repo", "Limits access to public repositories.",
127+
"repo:invite", "Grants accept/decline abilities for invitations to collaborate on a repository.",
128+
"security_events", "Grants read and write access to security events in the code scanning API.",
129+
"admin:repo_hook", "Grants read, write, ping, and delete access to repository hooks in public and private repositories.",
130+
"write:repo_hook", "Grants read, write, and ping access to hooks in public or private repositories.",
131+
"read:repo_hook", "Grants read and ping access to hooks in public or private repositories.",
132+
"admin:org", "Fully manage the organization and its teams, projects, and memberships.",
133+
"write:org", "Read and write access to organization membership, organization projects, and team membership.",
134+
"read:org", "Read-only access to organization membership, organization projects, and team membership.",
135+
"admin:public_key", "Fully manage public keys.",
136+
"write:public_key", "Create, list, and view details for public keys.",
137+
"read:public_key", "List and view details for public keys.",
138+
"admin:org_hook", "Grants read, write, ping, and delete access to organization hooks.",
139+
"gist", "Grants write access to gists.",
140+
"notifications", "Grants read access to a user's notifications",
141+
"user", "Grants read/write access to profile info only.",
142+
"read:user", "Grants access to read a user's profile data.",
143+
"user:email", "Grants read access to a user's email addresses.",
144+
"user:follow", "Grants access to follow or unfollow other users.",
145+
"delete_repo", "Grants access to delete adminable repositories.",
146+
"write:discussion", "Allows read and write access for team discussions.",
147+
"read:discussion", "Allows read access for team discussions.",
148+
"write:packages", "Grants access to upload or publish a package in GitHub Packages.",
149+
"read:packages", "Grants access to download or install packages from GitHub Packages.",
150+
"delete:packages", "Grants access to delete packages from GitHub Packages.",
151+
"admin:gpg_key", "Fully manage GPG keys.",
152+
"write:gpg_key", "Create, list, and view details for GPG keys.",
153+
"read:gpg_key", "List and view details for GPG keys.",
154+
"workflow", "Grants the ability to add and update GitHub Actions workflow files.",
155+
).Invoke(c).ToMultiPartsA(":")
156+
})
157+
}
158+
159+
// https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format/
160+
func byteCountSI(b int) string {
161+
const unit = 1000
162+
if b < unit {
163+
return fmt.Sprintf("%d B", b)
164+
}
165+
div, exp := int64(unit), 0
166+
for n := b / unit; n >= unit; n /= unit {
167+
div *= unit
168+
exp++
169+
}
170+
return fmt.Sprintf("%.1f %cB",
171+
float64(b)/float64(div), "kMGTPE"[exp])
172+
}
173+
174+
func defaultUser() (user string, err error) {
175+
var cfg config.Config
176+
if cfg, err = config.ParseDefaultConfig(); err == nil {
177+
var hosts []string
178+
if hosts, err = cfg.Hosts(); err == nil {
179+
if len(hosts) < 1 {
180+
err = errors.New("could not retrieve default user")
181+
} else {
182+
user, err = cfg.Get(hosts[0], "user")
183+
}
184+
}
185+
}
186+
return
187+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package action
2+
3+
import (
4+
"errors"
5+
6+
"github.com/rsteube/carapace"
7+
"github.com/rsteube/carapace-bin/completers/gh_completer/cmd/action/config"
8+
)
9+
10+
func Aliases() (map[string]string, error) {
11+
if config, err := config.ParseDefaultConfig(); err != nil {
12+
return nil, errors.New("failed to parse DefaultConfig:" + err.Error())
13+
} else {
14+
if aliasCfg, err := config.Aliases(); err != nil {
15+
return nil, errors.New("failed to load AliasCfg:" + err.Error())
16+
} else {
17+
aliases := make(map[string]string)
18+
for key, value := range aliasCfg.All() {
19+
aliases[key] = value
20+
}
21+
return aliases, nil
22+
}
23+
}
24+
}
25+
26+
func ActionAliases() carapace.Action {
27+
return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
28+
if aliases, err := Aliases(); err != nil {
29+
return carapace.ActionMessage(err.Error())
30+
} else {
31+
values := make([]string, 0)
32+
for key, value := range aliases {
33+
values = append(values, key, value)
34+
}
35+
return carapace.ActionValuesDescribed(values...)
36+
}
37+
})
38+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package action
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
"strings"
7+
8+
"github.com/rsteube/carapace"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
// ActionApiPreviews https://docs.github.com/en/rest/overview/api-previews
13+
func ActionApiPreviews() carapace.Action {
14+
return carapace.ActionValuesDescribed(
15+
"wyandotte", "Migrations",
16+
"ant-man", "Enhanced deployments",
17+
"squirrel-girl", "Reactions",
18+
"mockingbird", "Timeline",
19+
"inertia", "Projects",
20+
"cloak", "Commit search",
21+
"mercy", "Repository topics",
22+
"scarlet-witch", "Codes of conduct",
23+
"zzzax", "Require signed commits",
24+
"luke-cage", "Require multiple approving reviews",
25+
"starfox", "Project card details",
26+
"fury", "GitHub App Manifests",
27+
"flash", "Deployment statuses",
28+
"surtur", "Repository creation permissions",
29+
"corsair", "Content attachments",
30+
"switcheroo", "Enable and disable Pages",
31+
"groot", "List branches or pull requests for a commit",
32+
"dorian", "Enable or disable vulnerability alerts for a repository",
33+
"lydian", "Update a pull request branch",
34+
"london", "Enable or disable automated security fixes",
35+
"baptiste", "Create and use repository templates",
36+
"nebula", "New visibility parameter for the Repositories API",
37+
)
38+
}
39+
40+
func ActionApiV3Paths(cmd *cobra.Command) carapace.Action {
41+
return carapace.ActionMultiParts("/", func(c carapace.Context) carapace.Action {
42+
placeholder := regexp.MustCompile(`{(.*)}`)
43+
matchedData := make(map[string]string)
44+
matchedSegments := make(map[string]bool)
45+
staticMatches := make(map[int]bool)
46+
47+
path:
48+
for _, path := range v3Paths {
49+
segments := strings.Split(path, "/")
50+
segment:
51+
for index, segment := range segments {
52+
if index > len(c.Parts)-1 {
53+
break segment
54+
} else {
55+
if segment != c.Parts[index] {
56+
if !placeholder.MatchString(segment) {
57+
continue path // skip this path as it doesn't match and is not a placeholder
58+
} else {
59+
matchedData[segment] = c.Parts[index] // store entered data for placeholder (overwrite if duplicate)
60+
}
61+
} else {
62+
staticMatches[index] = true // static segment matches so placeholders should be ignored for this index
63+
}
64+
}
65+
}
66+
67+
if len(segments) < len(c.Parts)+1 {
68+
continue path // skip path as it is shorter than what was entered (must be after staticMatches being set)
69+
}
70+
71+
for key := range staticMatches {
72+
if segments[key] != c.Parts[key] {
73+
continue path // skip this path as it has a placeholder where a static segment was matched
74+
}
75+
}
76+
matchedSegments[segments[len(c.Parts)]] = true // store segment as path matched so far and this is currently being completed
77+
}
78+
79+
actions := make([]carapace.InvokedAction, 0, len(matchedSegments))
80+
for key := range matchedSegments {
81+
switch key {
82+
// TODO completion for other placeholders
83+
case "{archive_format}":
84+
actions = append(actions, carapace.ActionValues("zip").Invoke(c))
85+
case "{artifact_id}":
86+
fakeRepoFlag(cmd, matchedData["{owner}"], matchedData["{repo}"])
87+
actions = append(actions, ActionWorkflowArtifactIds(cmd, "").Invoke(c))
88+
case "{assignee}":
89+
fakeRepoFlag(cmd, matchedData["{owner}"], matchedData["{repo}"])
90+
actions = append(actions, ActionAssignableUsers(cmd).Invoke(c))
91+
case "{branch}":
92+
fakeRepoFlag(cmd, matchedData["{owner}"], matchedData["{repo}"])
93+
actions = append(actions, ActionBranches(cmd).Invoke(c))
94+
case "{gist_id}":
95+
actions = append(actions, ActionGists(cmd).Invoke(c))
96+
case "{gitignore_name}":
97+
actions = append(actions, ActionGitignoreTemplates(cmd).Invoke(c))
98+
case "{issue_number}":
99+
fakeRepoFlag(cmd, matchedData["{owner}"], matchedData["{repo}"])
100+
actions = append(actions, ActionIssues(cmd, IssueOpts{Open: true, Closed: true}).Invoke(c))
101+
case "{owner}":
102+
if strings.HasPrefix(c.CallbackValue, ":") {
103+
actions = append(actions, carapace.ActionValues(":owner").Invoke(c))
104+
} else {
105+
actions = append(actions, ActionUsers(cmd, UserOpts{Users: true, Organizations: true}).Invoke(c))
106+
}
107+
case "{org}":
108+
actions = append(actions, ActionUsers(cmd, UserOpts{Organizations: true}).Invoke(c))
109+
case "{package_type}":
110+
actions = append(actions, ActionPackageTypes().Invoke(c))
111+
case "{pull_number}":
112+
fakeRepoFlag(cmd, matchedData["{owner}"], matchedData["{repo}"])
113+
actions = append(actions, ActionPullRequests(cmd, PullRequestOpts{Open: true, Closed: true, Merged: true}).Invoke(c))
114+
case "{repo}":
115+
if strings.HasPrefix(c.CallbackValue, ":") {
116+
actions = append(actions, carapace.ActionValues(":repo").Invoke(c))
117+
} else {
118+
actions = append(actions, ActionRepositories(cmd, matchedData["{owner}"], c.CallbackValue).Invoke(c))
119+
}
120+
case "{tag}": // only used with releases
121+
fakeRepoFlag(cmd, matchedData["{owner}"], matchedData["{repo}"])
122+
actions = append(actions, ActionReleases(cmd).Invoke(c))
123+
case "{template_owner}": // ignore this as it is already provided by `{owner}`
124+
case "{template_repo}": // ignore this as it is already provided by `{repo}`
125+
case "{username}":
126+
actions = append(actions, ActionUsers(cmd, UserOpts{Users: true}).Invoke(c))
127+
case "{workflow_id}":
128+
fakeRepoFlag(cmd, matchedData["{owner}"], matchedData["{repo}"])
129+
actions = append(actions, ActionWorkflows(cmd, WorkflowOpts{Enabled: true, Disabled: true, Id: true}).Invoke(c))
130+
default:
131+
// static value or placeholder not yet handled
132+
actions = append(actions, carapace.ActionValues(key).Invoke(c))
133+
}
134+
}
135+
switch len(actions) {
136+
case 0:
137+
return carapace.ActionValues()
138+
case 1:
139+
return actions[0].ToA()
140+
default:
141+
return actions[0].Merge(actions[1:]...).ToA()
142+
}
143+
})
144+
}
145+
146+
func fakeRepoFlag(cmd *cobra.Command, owner, repo string) {
147+
cmd.Flags().String("repo", fmt.Sprintf("%v/%v", owner, repo), "fake repo flag")
148+
cmd.Flag("repo").Changed = true
149+
}

0 commit comments

Comments
 (0)