Merge remote-tracking branch 'origin/trunk' into more-gists
This commit is contained in:
commit
0af61ff1ff
13 changed files with 864 additions and 73 deletions
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
|
|
@ -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
28
.github/ISSUE_TEMPLATE/feedback.md
vendored
Normal 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"_
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
480
pkg/cmd/repo/garden/garden.go
Normal file
480
pkg/cmd/repo/garden/garden.go
Normal 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
105
pkg/cmd/repo/garden/http.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"]})
|
||||
|
|
|
|||
58
pkg/cmd/root/help_topic.go
Normal file
58
pkg/cmd/root/help_topic.go
Normal 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
|
||||
}
|
||||
79
pkg/cmd/root/help_topic_test.go
Normal file
79
pkg/cmd/root/help_topic_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue