222 lines
4.9 KiB
Go
222 lines
4.9 KiB
Go
package shared
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cli/cli/v2/api"
|
|
"github.com/cli/cli/v2/internal/prompter"
|
|
"github.com/cli/cli/v2/internal/text"
|
|
"github.com/cli/cli/v2/pkg/iostreams"
|
|
"github.com/gabriel-vasile/mimetype"
|
|
"github.com/shurcooL/githubv4"
|
|
)
|
|
|
|
type GistFile struct {
|
|
Filename string `json:"filename,omitempty"`
|
|
Type string `json:"type,omitempty"`
|
|
Language string `json:"language,omitempty"`
|
|
Content string `json:"content"`
|
|
}
|
|
|
|
type GistOwner struct {
|
|
Login string `json:"login,omitempty"`
|
|
}
|
|
|
|
type Gist struct {
|
|
ID string `json:"id,omitempty"`
|
|
Description string `json:"description"`
|
|
Files map[string]*GistFile `json:"files"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
Public bool `json:"public"`
|
|
HTMLURL string `json:"html_url,omitempty"`
|
|
Owner *GistOwner `json:"owner,omitempty"`
|
|
}
|
|
|
|
var NotFoundErr = errors.New("not found")
|
|
|
|
func GetGist(client *http.Client, hostname, gistID string) (*Gist, error) {
|
|
gist := Gist{}
|
|
path := fmt.Sprintf("gists/%s", gistID)
|
|
|
|
apiClient := api.NewClientFromHTTP(client)
|
|
err := apiClient.REST(hostname, "GET", path, nil, &gist)
|
|
if err != nil {
|
|
var httpErr api.HTTPError
|
|
if errors.As(err, &httpErr) && httpErr.StatusCode == 404 {
|
|
return nil, NotFoundErr
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
return &gist, nil
|
|
}
|
|
|
|
func GistIDFromURL(gistURL string) (string, error) {
|
|
u, err := url.Parse(gistURL)
|
|
if err == nil && strings.HasPrefix(u.Path, "/") {
|
|
split := strings.Split(u.Path, "/")
|
|
|
|
if len(split) > 2 {
|
|
return split[2], nil
|
|
}
|
|
|
|
if len(split) == 2 && split[1] != "" {
|
|
return split[1], nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("Invalid gist URL %s", u)
|
|
}
|
|
|
|
func ListGists(client *http.Client, hostname string, limit int, visibility string) ([]Gist, error) {
|
|
type response struct {
|
|
Viewer struct {
|
|
Gists struct {
|
|
Nodes []struct {
|
|
Description string
|
|
Files []struct {
|
|
Name string
|
|
}
|
|
IsPublic bool
|
|
Name string
|
|
UpdatedAt time.Time
|
|
}
|
|
PageInfo struct {
|
|
HasNextPage bool
|
|
EndCursor string
|
|
}
|
|
} `graphql:"gists(first: $per_page, after: $endCursor, privacy: $visibility, orderBy: {field: CREATED_AT, direction: DESC})"`
|
|
}
|
|
}
|
|
|
|
perPage := limit
|
|
if perPage > 100 {
|
|
perPage = 100
|
|
}
|
|
|
|
variables := map[string]interface{}{
|
|
"per_page": githubv4.Int(perPage),
|
|
"endCursor": (*githubv4.String)(nil),
|
|
"visibility": githubv4.GistPrivacy(strings.ToUpper(visibility)),
|
|
}
|
|
|
|
gql := api.NewClientFromHTTP(client)
|
|
|
|
gists := []Gist{}
|
|
pagination:
|
|
for {
|
|
var result response
|
|
err := gql.Query(hostname, "GistList", &result, variables)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, gist := range result.Viewer.Gists.Nodes {
|
|
files := map[string]*GistFile{}
|
|
for _, file := range gist.Files {
|
|
files[file.Name] = &GistFile{
|
|
Filename: file.Name,
|
|
}
|
|
}
|
|
|
|
gists = append(
|
|
gists,
|
|
Gist{
|
|
ID: gist.Name,
|
|
Description: gist.Description,
|
|
Files: files,
|
|
UpdatedAt: gist.UpdatedAt,
|
|
Public: gist.IsPublic,
|
|
},
|
|
)
|
|
if len(gists) == limit {
|
|
break pagination
|
|
}
|
|
}
|
|
|
|
if !result.Viewer.Gists.PageInfo.HasNextPage {
|
|
break
|
|
}
|
|
variables["endCursor"] = githubv4.String(result.Viewer.Gists.PageInfo.EndCursor)
|
|
}
|
|
|
|
return gists, nil
|
|
}
|
|
|
|
func IsBinaryFile(file string) (bool, error) {
|
|
detectedMime, err := mimetype.DetectFile(file)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
isBinary := true
|
|
for mime := detectedMime; mime != nil; mime = mime.Parent() {
|
|
if mime.Is("text/plain") {
|
|
isBinary = false
|
|
break
|
|
}
|
|
}
|
|
return isBinary, nil
|
|
}
|
|
|
|
func IsBinaryContents(contents []byte) bool {
|
|
isBinary := true
|
|
for mime := mimetype.Detect(contents); mime != nil; mime = mime.Parent() {
|
|
if mime.Is("text/plain") {
|
|
isBinary = false
|
|
break
|
|
}
|
|
}
|
|
return isBinary
|
|
}
|
|
|
|
func PromptGists(prompter prompter.Prompter, client *http.Client, host string, cs *iostreams.ColorScheme) (gistID string, err error) {
|
|
gists, err := ListGists(client, host, 10, "all")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if len(gists) == 0 {
|
|
return "", nil
|
|
}
|
|
|
|
var opts []string
|
|
var gistIDs = make([]string, len(gists))
|
|
|
|
for i, gist := range gists {
|
|
gistIDs[i] = gist.ID
|
|
description := ""
|
|
gistName := ""
|
|
|
|
if gist.Description != "" {
|
|
description = gist.Description
|
|
}
|
|
|
|
filenames := make([]string, 0, len(gist.Files))
|
|
for fn := range gist.Files {
|
|
filenames = append(filenames, fn)
|
|
}
|
|
sort.Strings(filenames)
|
|
gistName = filenames[0]
|
|
|
|
gistTime := text.FuzzyAgo(time.Now(), gist.UpdatedAt)
|
|
// TODO: support dynamic maxWidth
|
|
description = text.Truncate(100, text.RemoveExcessiveWhitespace(description))
|
|
opt := fmt.Sprintf("%s %s %s", cs.Bold(gistName), description, cs.Gray(gistTime))
|
|
opts = append(opts, opt)
|
|
}
|
|
|
|
result, err := prompter.Select("Select a gist", "", opts)
|
|
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return gistIDs[result], nil
|
|
}
|