Merge remote-tracking branch 'origin/trunk' into more-gists

This commit is contained in:
vilmibm 2020-09-15 13:25:06 -05:00
commit 0af61ff1ff
13 changed files with 864 additions and 73 deletions

View file

@ -27,7 +27,7 @@ Please avoid:
Prerequisites:
- Go 1.13+ for building the binary
- Go 1.14+ for running the test suite
- Go 1.15+ for running the test suite
Build with: `make` or `go build -o bin/gh ./cmd/gh`

28
.github/ISSUE_TEMPLATE/feedback.md vendored Normal file
View file

@ -0,0 +1,28 @@
---
name: "\U0001F4E3 Feedback"
about: Give us general feedback about the GitHub CLI
title: ''
labels: feedback
assignees: ''
---
# CLI Feedback
You can use this template to give us structured feedback or just wipe it and leave us a note. Thank you!
## What have you loved?
_eg "the nice colors"_
## What was confusing or gave you pause?
_eg "it did something unexpected"_
## Are there features you'd like to see added?
_eg "gh cli needs mini-games"_
## Anything else?
_eg "have a nice day"_

View file

@ -24,8 +24,11 @@ type LoginOptions struct {
IO *iostreams.IOStreams
Config func() (config.Config, error)
Interactive bool
Hostname string
Token string
Web bool
}
func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Command {
@ -58,6 +61,14 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm
# => read token from mytoken.txt and authenticate against a GitHub Enterprise Server instance
`),
RunE: func(cmd *cobra.Command, args []string) error {
if !opts.IO.CanPrompt() && !(tokenStdin || opts.Web) {
return &cmdutil.FlagError{Err: errors.New("--web or --with-token required when not running interactively")}
}
if tokenStdin && opts.Web {
return &cmdutil.FlagError{Err: errors.New("specify only one of --web or --with-token")}
}
if tokenStdin {
defer opts.IO.In.Close()
token, err := ioutil.ReadAll(opts.IO.In)
@ -67,15 +78,8 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm
opts.Token = strings.TrimSpace(string(token))
}
if opts.Token != "" {
// Assume non-interactive if a token is specified
if opts.Hostname == "" {
opts.Hostname = ghinstance.Default()
}
} else {
if !opts.IO.CanPrompt() {
return &cmdutil.FlagError{Err: errors.New("--with-token required when not running interactively")}
}
if opts.IO.CanPrompt() && opts.Token == "" && !opts.Web {
opts.Interactive = true
}
if cmd.Flags().Changed("hostname") {
@ -84,6 +88,12 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm
}
}
if !opts.Interactive {
if opts.Hostname == "" {
opts.Hostname = ghinstance.Default()
}
}
if runF != nil {
return runF(opts)
}
@ -94,6 +104,7 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm
cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "The hostname of the GitHub instance to authenticate with")
cmd.Flags().BoolVar(&tokenStdin, "with-token", false, "Read token from standard input")
cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "Open a browser to authenticate")
return cmd
}
@ -160,7 +171,7 @@ func loginRun(opts *LoginOptions) error {
existingToken, _ := cfg.Get(hostname, "oauth_token")
if existingToken != "" {
if existingToken != "" && opts.Interactive {
err := client.ValidateHostCfg(hostname, cfg)
if err == nil {
apiClient, err := client.ClientFromCfg(hostname, cfg)
@ -195,15 +206,19 @@ func loginRun(opts *LoginOptions) error {
}
var authMode int
err = prompt.SurveyAskOne(&survey.Select{
Message: "How would you like to authenticate?",
Options: []string{
"Login with a web browser",
"Paste an authentication token",
},
}, &authMode)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
if opts.Web {
authMode = 0
} else {
err = prompt.SurveyAskOne(&survey.Select{
Message: "How would you like to authenticate?",
Options: []string{
"Login with a web browser",
"Paste an authentication token",
},
}, &authMode)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
}
}
if authMode == 0 {
@ -239,28 +254,30 @@ func loginRun(opts *LoginOptions) error {
}
}
var gitProtocol string
err = prompt.SurveyAskOne(&survey.Select{
Message: "Choose default git protocol",
Options: []string{
"HTTPS",
"SSH",
},
}, &gitProtocol)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
gitProtocol := "https"
if opts.Interactive {
err = prompt.SurveyAskOne(&survey.Select{
Message: "Choose default git protocol",
Options: []string{
"HTTPS",
"SSH",
},
}, &gitProtocol)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
}
gitProtocol = strings.ToLower(gitProtocol)
fmt.Fprintf(opts.IO.ErrOut, "- gh config set -h %s git_protocol %s\n", hostname, gitProtocol)
err = cfg.Set(hostname, "git_protocol", gitProtocol)
if err != nil {
return err
}
fmt.Fprintf(opts.IO.ErrOut, "%s Configured git protocol\n", utils.GreenCheck())
}
gitProtocol = strings.ToLower(gitProtocol)
fmt.Fprintf(opts.IO.ErrOut, "- gh config set -h %s git_protocol %s\n", hostname, gitProtocol)
err = cfg.Set(hostname, "git_protocol", gitProtocol)
if err != nil {
return err
}
fmt.Fprintf(opts.IO.ErrOut, "%s Configured git protocol\n", utils.GreenCheck())
apiClient, err := client.ClientFromCfg(hostname, cfg)
if err != nil {
return err

View file

@ -81,8 +81,9 @@ func Test_NewCmdLogin(t *testing.T) {
stdinTTY: true,
cli: "--hostname barry.burton",
wants: LoginOptions{
Hostname: "barry.burton",
Token: "",
Hostname: "barry.burton",
Token: "",
Interactive: true,
},
},
{
@ -90,10 +91,33 @@ func Test_NewCmdLogin(t *testing.T) {
stdinTTY: true,
cli: "",
wants: LoginOptions{
Hostname: "",
Token: "",
Hostname: "",
Token: "",
Interactive: true,
},
},
{
name: "tty web",
stdinTTY: true,
cli: "--web",
wants: LoginOptions{
Hostname: "github.com",
Web: true,
},
},
{
name: "nontty web",
cli: "--web",
wants: LoginOptions{
Hostname: "github.com",
Web: true,
},
},
{
name: "web and with-token",
cli: "--web --with-token",
wantsErr: true,
},
}
for _, tt := range tests {
@ -134,6 +158,8 @@ func Test_NewCmdLogin(t *testing.T) {
assert.Equal(t, tt.wants.Token, gotOpts.Token)
assert.Equal(t, tt.wants.Hostname, gotOpts.Hostname)
assert.Equal(t, tt.wants.Web, gotOpts.Web)
assert.Equal(t, tt.wants.Interactive, gotOpts.Interactive)
})
}
}
@ -262,6 +288,9 @@ func Test_loginRun_Survey(t *testing.T) {
}{
{
name: "already authenticated",
opts: &LoginOptions{
Interactive: true,
},
cfg: func(cfg config.Config) {
_ = cfg.Set("github.com", "oauth_token", "ghi789")
},
@ -280,7 +309,8 @@ func Test_loginRun_Survey(t *testing.T) {
{
name: "hostname set",
opts: &LoginOptions{
Hostname: "rebecca.chambers",
Hostname: "rebecca.chambers",
Interactive: true,
},
wantHosts: "rebecca.chambers:\n oauth_token: def456\n git_protocol: https\n user: jillv\n",
askStubs: func(as *prompt.AskStubber) {
@ -298,6 +328,9 @@ func Test_loginRun_Survey(t *testing.T) {
{
name: "choose enterprise",
wantHosts: "brad.vickers:\n oauth_token: def456\n git_protocol: https\n user: jillv\n",
opts: &LoginOptions{
Interactive: true,
},
askStubs: func(as *prompt.AskStubber) {
as.StubOne(1) // host type enterprise
as.StubOne("brad.vickers") // hostname
@ -315,6 +348,9 @@ func Test_loginRun_Survey(t *testing.T) {
{
name: "choose github.com",
wantHosts: "github.com:\n oauth_token: def456\n git_protocol: https\n user: jillv\n",
opts: &LoginOptions{
Interactive: true,
},
askStubs: func(as *prompt.AskStubber) {
as.StubOne(0) // host type github.com
as.StubOne(1) // auth mode: token
@ -325,6 +361,9 @@ func Test_loginRun_Survey(t *testing.T) {
{
name: "sets git_protocol",
wantHosts: "github.com:\n oauth_token: def456\n git_protocol: ssh\n user: jillv\n",
opts: &LoginOptions{
Interactive: true,
},
askStubs: func(as *prompt.AskStubber) {
as.StubOne(0) // host type github.com
as.StubOne(1) // auth mode: token

View file

@ -18,6 +18,7 @@ func NewCmdConfig(f *cmdutil.Factory) *cobra.Command {
Current respected settings:
- git_protocol: "https" or "ssh". Default is "https".
- editor: if unset, defaults to environment variables.
- prompt: "enabled" or "disabled". Toggles interactive prompting.
`),
}
@ -72,6 +73,8 @@ func NewCmdConfigSet(f *cmdutil.Factory) *cobra.Command {
Example: heredoc.Doc(`
$ gh config set editor vim
$ gh config set editor "code --wait"
$ gh config set git_protocol ssh
$ gh config set prompt disabled
`),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {

View file

@ -24,7 +24,7 @@ func NewHTTPClient(io *iostreams.IOStreams, cfg config.Config, appVersion string
api.AddHeader("User-Agent", fmt.Sprintf("GitHub CLI %s", appVersion)),
api.AddHeaderFunc("Authorization", func(req *http.Request) (string, error) {
hostname := ghinstance.NormalizeHostname(req.URL.Hostname())
if token, err := cfg.Get(hostname, "oauth_token"); err == nil || token != "" {
if token, err := cfg.Get(hostname, "oauth_token"); err == nil && token != "" {
return fmt.Sprintf("token %s", token), nil
}
return "", nil

View file

@ -0,0 +1,480 @@
package garden
import (
"bytes"
"errors"
"fmt"
"io"
"math/rand"
"net/http"
"os/exec"
"runtime"
"strconv"
"strings"
"github.com/cli/cli/api"
"github.com/cli/cli/internal/ghinstance"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/iostreams"
"github.com/cli/cli/utils"
"github.com/spf13/cobra"
)
type Geometry struct {
Width int
Height int
Density float64
Repository ghrepo.Interface
}
type Player struct {
X int
Y int
Char string
Geo *Geometry
ShoeMoistureContent int
}
type Commit struct {
Email string
Handle string
Sha string
Char string
}
type Cell struct {
Char string
StatusLine string
}
const (
DirUp = iota
DirDown
DirLeft
DirRight
)
type Direction = int
func (p *Player) move(direction Direction) bool {
switch direction {
case DirUp:
if p.Y == 0 {
return false
}
p.Y--
case DirDown:
if p.Y == p.Geo.Height-1 {
return false
}
p.Y++
case DirLeft:
if p.X == 0 {
return false
}
p.X--
case DirRight:
if p.X == p.Geo.Width-1 {
return false
}
p.X++
}
return true
}
type GardenOptions struct {
HttpClient func() (*http.Client, error)
IO *iostreams.IOStreams
BaseRepo func() (ghrepo.Interface, error)
RepoArg string
}
func NewCmdGarden(f *cmdutil.Factory, runF func(*GardenOptions) error) *cobra.Command {
opts := GardenOptions{
IO: f.IOStreams,
HttpClient: f.HttpClient,
BaseRepo: f.BaseRepo,
}
cmd := &cobra.Command{
Use: "garden [<repository>]",
Short: "Explore a git repository as a garden",
Long: "Use arrow keys, WASD or vi keys to move. q to quit.",
Hidden: true,
RunE: func(c *cobra.Command, args []string) error {
if len(args) > 0 {
opts.RepoArg = args[0]
}
if runF != nil {
return runF(&opts)
}
return gardenRun(&opts)
},
}
return cmd
}
func gardenRun(opts *GardenOptions) error {
out := opts.IO.Out
if runtime.GOOS == "windows" {
return errors.New("sorry :( this command only works on linux and macos")
}
if !opts.IO.IsStdoutTTY() {
return errors.New("must be connected to a terminal")
}
httpClient, err := opts.HttpClient()
if err != nil {
return err
}
var toView ghrepo.Interface
apiClient := api.NewClientFromHTTP(httpClient)
if opts.RepoArg == "" {
var err error
toView, err = opts.BaseRepo()
if err != nil {
return err
}
} else {
var err error
viewURL := opts.RepoArg
if !strings.Contains(viewURL, "/") {
currentUser, err := api.CurrentLoginName(apiClient, ghinstance.Default())
if err != nil {
return err
}
viewURL = currentUser + "/" + viewURL
}
toView, err = ghrepo.FromFullName(viewURL)
if err != nil {
return fmt.Errorf("argument error: %w", err)
}
}
seed := computeSeed(ghrepo.FullName(toView))
rand.Seed(seed)
termWidth, termHeight, err := utils.TerminalSize(out)
if err != nil {
return err
}
termWidth -= 10
termHeight -= 10
geo := &Geometry{
Width: termWidth,
Height: termHeight,
Repository: toView,
// TODO based on number of commits/cells instead of just hardcoding
Density: 0.3,
}
maxCommits := (geo.Width * geo.Height) / 2
opts.IO.StartProgressIndicator()
fmt.Fprintln(out, "gathering commits; this could take a minute...")
commits, err := getCommits(httpClient, toView, maxCommits)
opts.IO.StopProgressIndicator()
if err != nil {
return err
}
player := &Player{0, 0, utils.Bold("@"), geo, 0}
garden := plantGarden(commits, geo)
clear(opts.IO)
drawGarden(out, garden, player)
// thanks stackoverflow https://stackoverflow.com/a/17278776
if runtime.GOOS == "darwin" {
_ = exec.Command("stty", "-f", "/dev/tty", "cbreak", "min", "1").Run()
_ = exec.Command("stty", "-f", "/dev/tty", "-echo").Run()
} else {
_ = exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run()
_ = exec.Command("stty", "-F", "/dev/tty", "-echo").Run()
}
var b []byte = make([]byte, 3)
for {
_, _ = opts.IO.In.Read(b)
oldX := player.X
oldY := player.Y
moved := false
quitting := false
continuing := false
switch {
case isLeft(b):
moved = player.move(DirLeft)
case isRight(b):
moved = player.move(DirRight)
case isUp(b):
moved = player.move(DirUp)
case isDown(b):
moved = player.move(DirDown)
case isQuit(b):
quitting = true
default:
continuing = true
}
if quitting {
break
}
if !moved || continuing {
continue
}
underPlayer := garden[player.Y][player.X]
previousCell := garden[oldY][oldX]
// print whatever was just under player
fmt.Fprint(out, "\033[;H") // move to top left
for x := 0; x < oldX && x < player.Geo.Width; x++ {
fmt.Fprint(out, "\033[C")
}
for y := 0; y < oldY && y < player.Geo.Height; y++ {
fmt.Fprint(out, "\033[B")
}
fmt.Fprint(out, previousCell.Char)
// print player character
fmt.Fprint(out, "\033[;H") // move to top left
for x := 0; x < player.X && x < player.Geo.Width; x++ {
fmt.Fprint(out, "\033[C")
}
for y := 0; y < player.Y && y < player.Geo.Height; y++ {
fmt.Fprint(out, "\033[B")
}
fmt.Fprint(out, player.Char)
// handle stream wettening
if strings.Contains(underPlayer.StatusLine, "stream") {
player.ShoeMoistureContent = 5
} else {
if player.ShoeMoistureContent > 0 {
player.ShoeMoistureContent--
}
}
// status line stuff
sl := statusLine(garden, player)
fmt.Fprint(out, "\033[;H") // move to top left
for y := 0; y < player.Geo.Height-1; y++ {
fmt.Fprint(out, "\033[B")
}
fmt.Fprintln(out)
fmt.Fprintln(out)
fmt.Fprint(out, utils.Bold(sl))
}
clear(opts.IO)
fmt.Fprint(out, "\033[?25h")
fmt.Fprintln(out)
fmt.Fprintln(out, utils.Bold("You turn and walk away from the wildflower garden..."))
return nil
}
func isLeft(b []byte) bool {
left := []byte{27, 91, 68}
r := rune(b[0])
return bytes.EqualFold(b, left) || r == 'a' || r == 'h'
}
func isRight(b []byte) bool {
right := []byte{27, 91, 67}
r := rune(b[0])
return bytes.EqualFold(b, right) || r == 'd' || r == 'l'
}
func isDown(b []byte) bool {
down := []byte{27, 91, 66}
r := rune(b[0])
return bytes.EqualFold(b, down) || r == 's' || r == 'j'
}
func isUp(b []byte) bool {
up := []byte{27, 91, 65}
r := rune(b[0])
return bytes.EqualFold(b, up) || r == 'w' || r == 'k'
}
func isQuit(b []byte) bool {
return rune(b[0]) == 'q'
}
func plantGarden(commits []*Commit, geo *Geometry) [][]*Cell {
cellIx := 0
grassCell := &Cell{RGB(0, 200, 0, ","), "You're standing on a patch of grass in a field of wildflowers."}
garden := [][]*Cell{}
streamIx := rand.Intn(geo.Width - 1)
if streamIx == geo.Width/2 {
streamIx--
}
tint := 0
for y := 0; y < geo.Height; y++ {
if cellIx == len(commits)-1 {
break
}
garden = append(garden, []*Cell{})
for x := 0; x < geo.Width; x++ {
if (y > 0 && (x == 0 || x == geo.Width-1)) || y == geo.Height-1 {
garden[y] = append(garden[y], &Cell{
Char: RGB(0, 150, 0, "^"),
StatusLine: "You're standing under a tall, leafy tree.",
})
continue
}
if x == streamIx {
garden[y] = append(garden[y], &Cell{
Char: RGB(tint, tint, 255, "#"),
StatusLine: "You're standing in a shallow stream. It's refreshing.",
})
tint += 15
streamIx--
if rand.Float64() < 0.5 {
streamIx++
}
if streamIx < 0 {
streamIx = 0
}
if streamIx > geo.Width {
streamIx = geo.Width
}
continue
}
if y == 0 && (x < geo.Width/2 || x > geo.Width/2) {
garden[y] = append(garden[y], &Cell{
Char: RGB(0, 200, 0, ","),
StatusLine: "You're standing by a wildflower garden. There is a light breeze.",
})
continue
} else if y == 0 && x == geo.Width/2 {
garden[y] = append(garden[y], &Cell{
Char: RGB(139, 69, 19, "+"),
StatusLine: fmt.Sprintf("You're standing in front of a weather-beaten sign that says %s.", ghrepo.FullName(geo.Repository)),
})
continue
}
if cellIx == len(commits)-1 {
garden[y] = append(garden[y], grassCell)
continue
}
chance := rand.Float64()
if chance <= geo.Density {
commit := commits[cellIx]
garden[y] = append(garden[y], &Cell{
Char: commits[cellIx].Char,
StatusLine: fmt.Sprintf("You're standing at a flower called %s planted by %s.", commit.Sha[0:6], commit.Handle),
})
cellIx++
} else {
garden[y] = append(garden[y], grassCell)
}
}
}
return garden
}
func drawGarden(out io.Writer, garden [][]*Cell, player *Player) {
fmt.Fprint(out, "\033[?25l") // hide cursor. it needs to be restored at command exit.
sl := ""
for y, gardenRow := range garden {
for x, gardenCell := range gardenRow {
char := ""
underPlayer := (player.X == x && player.Y == y)
if underPlayer {
sl = gardenCell.StatusLine
char = utils.Bold(player.Char)
if strings.Contains(gardenCell.StatusLine, "stream") {
player.ShoeMoistureContent = 5
}
} else {
char = gardenCell.Char
}
fmt.Fprint(out, char)
}
fmt.Fprintln(out)
}
fmt.Println()
fmt.Fprintln(out, utils.Bold(sl))
}
func statusLine(garden [][]*Cell, player *Player) string {
statusLine := garden[player.Y][player.X].StatusLine + " "
if player.ShoeMoistureContent > 1 {
statusLine += "\nYour shoes squish with water from the stream."
} else if player.ShoeMoistureContent == 1 {
statusLine += "\nYour shoes seem to have dried out."
} else {
statusLine += "\n "
}
return statusLine
}
func shaToColorFunc(sha string) func(string) string {
return func(c string) string {
red, err := strconv.ParseInt(sha[0:2], 16, 64)
if err != nil {
panic(err)
}
green, err := strconv.ParseInt(sha[2:4], 16, 64)
if err != nil {
panic(err)
}
blue, err := strconv.ParseInt(sha[4:6], 16, 64)
if err != nil {
panic(err)
}
return fmt.Sprintf("\033[38;2;%d;%d;%dm%s\033[0m", red, green, blue, c)
}
}
func computeSeed(seed string) int64 {
lol := ""
for _, r := range seed {
lol += fmt.Sprintf("%d", int(r))
}
result, err := strconv.ParseInt(lol[0:10], 10, 64)
if err != nil {
panic(err)
}
return result
}
func clear(io *iostreams.IOStreams) {
cmd := exec.Command("clear")
cmd.Stdout = io.Out
_ = cmd.Run()
}
func RGB(r, g, b int, x string) string {
return fmt.Sprintf("\033[38;2;%d;%d;%dm%s\033[0m", r, g, b, x)
}

105
pkg/cmd/repo/garden/http.go Normal file
View file

@ -0,0 +1,105 @@
package garden
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"
"github.com/cli/cli/internal/ghinstance"
"github.com/cli/cli/internal/ghrepo"
)
func getCommits(client *http.Client, repo ghrepo.Interface, maxCommits int) ([]*Commit, error) {
type Item struct {
Author struct {
Login string
}
Sha string
}
type Result []Item
commits := []*Commit{}
pathF := func(page int) string {
return fmt.Sprintf("repos/%s/%s/commits?per_page=100&page=%d", repo.RepoOwner(), repo.RepoName(), page)
}
page := 1
paginating := true
for paginating {
if len(commits) >= maxCommits {
break
}
result := Result{}
resp, err := getResponse(client, pathF(page), &result)
if err != nil {
return nil, err
}
for _, r := range result {
colorFunc := shaToColorFunc(r.Sha)
handle := r.Author.Login
if handle == "" {
handle = "a mysterious stranger"
}
commits = append(commits, &Commit{
Handle: handle,
Sha: r.Sha,
Char: colorFunc(string(handle[0])),
})
}
link := resp.Header["Link"]
if !strings.Contains(link[0], "last") {
paginating = false
}
page++
time.Sleep(500)
}
// reverse to get older commits first
for i, j := 0, len(commits)-1; i < j; i, j = i+1, j-1 {
commits[i], commits[j] = commits[j], commits[i]
}
return commits, nil
}
func getResponse(client *http.Client, path string, data interface{}) (*http.Response, error) {
url := ghinstance.RESTPrefix(ghinstance.OverridableDefault()) + path
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
success := resp.StatusCode >= 200 && resp.StatusCode < 300
if !success {
return nil, errors.New("api call failed")
}
if resp.StatusCode == http.StatusNoContent {
return resp, nil
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
err = json.Unmarshal(b, &data)
if err != nil {
return nil, err
}
return resp, nil
}

View file

@ -6,6 +6,7 @@ import (
repoCreateCmd "github.com/cli/cli/pkg/cmd/repo/create"
creditsCmd "github.com/cli/cli/pkg/cmd/repo/credits"
repoForkCmd "github.com/cli/cli/pkg/cmd/repo/fork"
gardenCmd "github.com/cli/cli/pkg/cmd/repo/garden"
repoViewCmd "github.com/cli/cli/pkg/cmd/repo/view"
"github.com/cli/cli/pkg/cmdutil"
"github.com/spf13/cobra"
@ -36,6 +37,7 @@ func NewCmdRepo(f *cmdutil.Factory) *cobra.Command {
cmd.AddCommand(repoCloneCmd.NewCmdClone(f, nil))
cmd.AddCommand(repoCreateCmd.NewCmdCreate(f, nil))
cmd.AddCommand(creditsCmd.NewCmdRepoCredits(f, nil))
cmd.AddCommand(gardenCmd.NewCmdGarden(f, nil))
return cmd
}

View file

@ -139,7 +139,7 @@ func rootHelpFunc(command *cobra.Command, args []string) {
helpEntries = append(helpEntries, helpEntry{"ENVIRONMENT VARIABLES", command.Annotations["help:environment"]})
}
helpEntries = append(helpEntries, helpEntry{"LEARN MORE", `
Use "gh <command> <subcommand> --help" for more information about a command.
Use 'gh <command> <subcommand> --help' for more information about a command.
Read the manual at https://cli.github.com/manual`})
if _, ok := command.Annotations["help:feedback"]; ok {
helpEntries = append(helpEntries, helpEntry{"FEEDBACK", command.Annotations["help:feedback"]})

View file

@ -0,0 +1,58 @@
package root
import (
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
)
func NewHelpTopic(topic string) *cobra.Command {
topicContent := make(map[string]string)
topicContent["environment"] = heredoc.Doc(`
GITHUB_TOKEN: an authentication token for github.com API requests. Setting this avoids
being prompted to authenticate and takes precedence over previously stored credentials.
GITHUB_ENTERPRISE_TOKEN: an authentication token for API requests to GitHub Enterprise.
GH_REPO: specify the GitHub repository in the "[HOST/]OWNER/REPO" format for commands
that otherwise operate on a local repository.
GH_HOST: specify the GitHub hostname for commands that would otherwise assume
the "github.com" host when not in a context of an existing repository.
GH_EDITOR, GIT_EDITOR, VISUAL, EDITOR (in order of precedence): the editor tool to use
for authoring text.
BROWSER: the web browser to use for opening links.
DEBUG: set to any value to enable verbose output to standard error. Include values "api"
or "oauth" to print detailed information about HTTP requests or authentication flow.
GLAMOUR_STYLE: the style to use for rendering Markdown. See
https://github.com/charmbracelet/glamour#styles
NO_COLOR: avoid printing ANSI escape sequences for color output.
`)
cmd := &cobra.Command{
Use: topic,
Long: topicContent[topic],
Hidden: true,
Args: cobra.NoArgs,
Run: helpTopicHelpFunc,
}
cmd.SetHelpFunc(helpTopicHelpFunc)
cmd.SetUsageFunc(helpTopicUsageFunc)
return cmd
}
func helpTopicHelpFunc(command *cobra.Command, args []string) {
command.Print(command.Long)
}
func helpTopicUsageFunc(command *cobra.Command) error {
command.Printf("Usage: gh help %s", command.Use)
return nil
}

View file

@ -0,0 +1,79 @@
package root
import (
"testing"
"github.com/cli/cli/pkg/iostreams"
"github.com/stretchr/testify/assert"
)
func TestNewHelpTopic(t *testing.T) {
tests := []struct {
name string
topic string
args []string
flags []string
wantsErr bool
}{
{
name: "valid topic",
topic: "environment",
args: []string{},
flags: []string{},
wantsErr: false,
},
{
name: "invalid topic",
topic: "invalid",
args: []string{},
flags: []string{},
wantsErr: false,
},
{
name: "more than zero args",
topic: "environment",
args: []string{"invalid"},
flags: []string{},
wantsErr: true,
},
{
name: "more than zero flags",
topic: "environment",
args: []string{},
flags: []string{"--invalid"},
wantsErr: true,
},
{
name: "help arg",
topic: "environment",
args: []string{"help"},
flags: []string{},
wantsErr: true,
},
{
name: "help flag",
topic: "environment",
args: []string{},
flags: []string{"--help"},
wantsErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, _, stdout, stderr := iostreams.Test()
cmd := NewHelpTopic(tt.topic)
cmd.SetArgs(append(tt.args, tt.flags...))
cmd.SetOut(stdout)
cmd.SetErr(stderr)
_, err := cmd.ExecuteC()
if tt.wantsErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
})
}
}

View file

@ -41,32 +41,10 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command {
`),
Annotations: map[string]string{
"help:feedback": heredoc.Doc(`
Open an issue using gh issue create -R cli/cli
Open an issue using 'gh issue create -R cli/cli'
`),
"help:environment": heredoc.Doc(`
GITHUB_TOKEN: an authentication token for github.com API requests. Setting this avoids
being prompted to authenticate and takes precedence over previously stored credentials.
GITHUB_ENTERPRISE_TOKEN: an authentication token for API requests to GitHub Enterprise.
GH_REPO: specify the GitHub repository in the "[HOST/]OWNER/REPO" format for commands
that otherwise operate on a local repository.
GH_HOST: specify the GitHub hostname for commands that would otherwise assume
the "github.com" host when not in a context of an existing repository.
GH_EDITOR, GIT_EDITOR, VISUAL, EDITOR (in order of precedence): the editor tool to use
for authoring text.
BROWSER: the web browser to use for opening links.
DEBUG: set to any value to enable verbose output to standard error. Include values "api"
or "oauth" to print detailed information about HTTP requests or authentication flow.
GLAMOUR_STYLE: the style to use for rendering Markdown. See
https://github.com/charmbracelet/glamour#styles
NO_COLOR: avoid printing ANSI escape sequences for color output.
See 'gh help environment' for the list of supported environment variables.
`),
},
}
@ -104,8 +82,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command {
cmdutil.DisableAuthCheck(cmd)
// CHILD COMMANDS
// Child commands
cmd.AddCommand(aliasCmd.NewCmdAlias(f))
cmd.AddCommand(authCmd.NewCmdAuth(f))
cmd.AddCommand(configCmd.NewCmdConfig(f))
@ -113,6 +90,9 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command {
cmd.AddCommand(gistCmd.NewCmdGist(f))
cmd.AddCommand(NewCmdCompletion(f.IOStreams))
// Help topics
cmd.AddCommand(NewHelpTopic("environment"))
// the `api` command should not inherit any extra HTTP headers
bareHTTPCmdFactory := *f
bareHTTPCmdFactory.HttpClient = func() (*http.Client, error) {