Merge remote-tracking branch 'origin/master' into upgrade-gh-reminder
This commit is contained in:
commit
5ce2b7ea18
23 changed files with 715 additions and 657 deletions
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
name: "\U0001F41B Bug report"
|
||||
about: Report a bug or unexpected behavior while using GitHub CLI
|
||||
|
||||
---
|
||||
|
||||
### Describe the bug
|
||||
|
||||
A clear and concise description of what the bug is. Include version by typing `gh --version`.
|
||||
|
||||
### Steps to reproduce the behavior
|
||||
|
||||
1. Type this '...'
|
||||
2. View the output '....'
|
||||
3. See error
|
||||
|
||||
### Expected vs actual behavior
|
||||
|
||||
A clear and concise description of what you expected to happen and what actually happened.
|
||||
|
||||
### Logs
|
||||
|
||||
Paste the activity from your command line. Redact if needed.
|
||||
17
.github/ISSUE_TEMPLATE/submit-a-request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/submit-a-request.md
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
name: "\U00002B50 Submit a request"
|
||||
about: Surface a feature or problem that you think should be solved
|
||||
|
||||
---
|
||||
|
||||
### Describe the feature or problem you’d like to solve
|
||||
|
||||
A clear and concise description of what the feature or problem is.
|
||||
|
||||
### Proposed solution
|
||||
|
||||
How will it benefit CLI and its users?
|
||||
|
||||
### Additional context
|
||||
|
||||
Add any other context like screenshots or mockups are helpful, if applicable.
|
||||
|
|
@ -24,6 +24,18 @@ archives:
|
|||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
|
||||
nfpms:
|
||||
- license: MIT
|
||||
maintainer: GitHub
|
||||
homepage: https://github.com/github/gh-cli
|
||||
bindir: /usr/local
|
||||
dependencies:
|
||||
- git
|
||||
formats:
|
||||
- deb
|
||||
- rpm
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
|
|
|
|||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2019 GitHub Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
2
Makefile
2
Makefile
|
|
@ -1,7 +1,7 @@
|
|||
BUILD_FILES = $(shell go list -f '{{range .GoFiles}}{{$$.Dir}}/{{.}}\
|
||||
{{end}}' ./...)
|
||||
|
||||
GH_VERSION = $(shell go describe --tags 2>/dev/null || git rev-parse --short HEAD)
|
||||
GH_VERSION = $(shell git describe --tags 2>/dev/null || git rev-parse --short HEAD)
|
||||
LDFLAGS := -X github.com/github/gh-cli/command.Version=$(GH_VERSION) $(LDFLAGS)
|
||||
LDFLAGS := -X github.com/github/gh-cli/command.BuildDate=$(shell date +%Y-%m-%d) $(LDFLAGS)
|
||||
ifdef GH_OAUTH_CLIENT_SECRET
|
||||
|
|
|
|||
36
README.md
36
README.md
|
|
@ -8,9 +8,32 @@ This tool is an endeavor separate from [github/hub](https://github.com/github/hu
|
|||
|
||||
_warning, gh is in a very alpha phase_
|
||||
|
||||
## macOS
|
||||
|
||||
`brew install github/gh/gh`
|
||||
|
||||
That's it. You are now ready to use `gh` on the command line. 🥳
|
||||
## Debian/Ubuntu Linux
|
||||
|
||||
1. `sudo apt install git` if you don't already have git
|
||||
2. Download the `.deb` file from the [releases page](https://github.com/github/gh-cli/releases/latest)
|
||||
3. `sudo dpkg -i gh_*_linux_amd64.deb` install the downloaded file
|
||||
|
||||
_(Uninstall with `sudo apt remove gh`)_
|
||||
|
||||
## Fedora/Centos Linux
|
||||
|
||||
1. Download the `.rpm` file from the [releases page](https://github.com/github/gh-cli/releases/latest)
|
||||
2. `sudo yum localinstall gh_*_linux_amd64.rpm` install the downloaded file
|
||||
|
||||
_(Uninstall with `sudo yum remove gh`)_
|
||||
|
||||
## Other Linux
|
||||
|
||||
1. Download the `_linux_amd64.tar.gz` file from the [releases page](https://github.com/github/gh-cli/releases/latest)
|
||||
2. `tar -xf gh_*_linux_amd64.tar.gz`
|
||||
3. Copy the uncompressed `gh` somewhere on your `$PATH` (e.g. `sudo cp gh_*_linux_amd64/bin/gh /usr/local/bin/`)
|
||||
|
||||
_(Uninstall with `rm`)_
|
||||
|
||||
# Process
|
||||
|
||||
|
|
@ -25,4 +48,13 @@ This can all be done from your local terminal.
|
|||
1. `git tag 'vVERSION_NUMBER' # example git tag 'v0.0.1'`
|
||||
2. `git push origin vVERSION_NUMBER`
|
||||
3. Wait a few minutes for the build to run and CI to pass. Look at the [actions tab](https://github.com/github/gh-cli/actions) to check the progress.
|
||||
4. Go to https://github.com/github/homebrew-gh/releases and look at the release
|
||||
4. Go to <https://github.com/github/homebrew-gh/releases> and look at the release
|
||||
|
||||
# Test a release
|
||||
|
||||
A local release can be created for testing without creating anything official on the release page.
|
||||
|
||||
1. `git tag 'v6.6.6' # some throwaway version number`
|
||||
2. `env GH_OAUTH_CLIENT_SECRET=foobar GH_OAUTH_CLIENT_ID=1234 goreleaser --skip-publish --rm-dist`
|
||||
3. Check and test files in `dist/`
|
||||
4. `git tag -d v6.6.6 # delete the throwaway tag`
|
||||
|
|
|
|||
|
|
@ -2,40 +2,33 @@ package api
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type IssuesPayload struct {
|
||||
Assigned []Issue
|
||||
Mentioned []Issue
|
||||
Recent []Issue
|
||||
Authored []Issue
|
||||
}
|
||||
|
||||
type Issue struct {
|
||||
Number int
|
||||
Title string
|
||||
URL string
|
||||
Labels []string
|
||||
TotalLabelCount int
|
||||
Number int
|
||||
Title string
|
||||
URL string
|
||||
State string
|
||||
|
||||
Labels struct {
|
||||
Nodes []IssueLabel
|
||||
TotalCount int
|
||||
}
|
||||
}
|
||||
|
||||
type IssueLabel struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type apiIssues struct {
|
||||
Issues struct {
|
||||
Edges []struct {
|
||||
Node struct {
|
||||
Number int
|
||||
Title string
|
||||
URL string
|
||||
Labels struct {
|
||||
Edges []struct {
|
||||
Node struct {
|
||||
Name string
|
||||
}
|
||||
}
|
||||
TotalCount int
|
||||
}
|
||||
}
|
||||
}
|
||||
Nodes []Issue
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -43,11 +36,11 @@ const fragments = `
|
|||
fragment issue on Issue {
|
||||
number
|
||||
title
|
||||
url
|
||||
state
|
||||
labels(first: 3) {
|
||||
edges {
|
||||
node {
|
||||
name
|
||||
}
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
|
|
@ -97,35 +90,29 @@ func IssueStatus(client *Client, ghRepo Repo, currentUsername string) (*IssuesPa
|
|||
type response struct {
|
||||
Assigned apiIssues
|
||||
Mentioned apiIssues
|
||||
Recent apiIssues
|
||||
Authored apiIssues
|
||||
}
|
||||
|
||||
query := fragments + `
|
||||
query($owner: String!, $repo: String!, $since: DateTime!, $viewer: String!, $per_page: Int = 10) {
|
||||
query($owner: String!, $repo: String!, $viewer: String!, $per_page: Int = 10) {
|
||||
assigned: repository(owner: $owner, name: $repo) {
|
||||
issues(filterBy: {assignee: $viewer, states: OPEN}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
|
||||
edges {
|
||||
node {
|
||||
...issue
|
||||
}
|
||||
nodes {
|
||||
...issue
|
||||
}
|
||||
}
|
||||
}
|
||||
mentioned: repository(owner: $owner, name: $repo) {
|
||||
issues(filterBy: {mentioned: $viewer, states: OPEN}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
|
||||
edges {
|
||||
node {
|
||||
...issue
|
||||
}
|
||||
nodes {
|
||||
...issue
|
||||
}
|
||||
}
|
||||
}
|
||||
recent: repository(owner: $owner, name: $repo) {
|
||||
issues(filterBy: {since: $since, states: OPEN}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
|
||||
edges {
|
||||
node {
|
||||
...issue
|
||||
}
|
||||
authored: repository(owner: $owner, name: $repo) {
|
||||
issues(filterBy: {createdBy: $viewer, states: OPEN}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
|
||||
nodes {
|
||||
...issue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -134,12 +121,10 @@ func IssueStatus(client *Client, ghRepo Repo, currentUsername string) (*IssuesPa
|
|||
|
||||
owner := ghRepo.RepoOwner()
|
||||
repo := ghRepo.RepoName()
|
||||
since := time.Now().UTC().Add(time.Hour * -24).Format("2006-01-02T15:04:05-0700")
|
||||
variables := map[string]interface{}{
|
||||
"owner": owner,
|
||||
"repo": repo,
|
||||
"viewer": currentUsername,
|
||||
"since": since,
|
||||
}
|
||||
|
||||
var resp response
|
||||
|
|
@ -148,14 +133,10 @@ func IssueStatus(client *Client, ghRepo Repo, currentUsername string) (*IssuesPa
|
|||
return nil, err
|
||||
}
|
||||
|
||||
assigned := convertAPIToIssues(resp.Assigned)
|
||||
mentioned := convertAPIToIssues(resp.Mentioned)
|
||||
recent := convertAPIToIssues(resp.Recent)
|
||||
|
||||
payload := IssuesPayload{
|
||||
assigned,
|
||||
mentioned,
|
||||
recent,
|
||||
Assigned: resp.Assigned.Issues.Nodes,
|
||||
Mentioned: resp.Mentioned.Issues.Nodes,
|
||||
Authored: resp.Authored.Issues.Nodes,
|
||||
}
|
||||
|
||||
return &payload, nil
|
||||
|
|
@ -191,10 +172,8 @@ func IssueList(client *Client, ghRepo Repo, state string, labels []string, assig
|
|||
query($owner: String!, $repo: String!, $limit: Int, $states: [IssueState!] = OPEN, $labels: [String!], $assignee: String) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
issues(first: $limit, orderBy: {field: CREATED_AT, direction: DESC}, states: $states, labels: $labels, filterBy: {assignee: $assignee}) {
|
||||
edges {
|
||||
node {
|
||||
...issue
|
||||
}
|
||||
nodes {
|
||||
...issue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -221,27 +200,5 @@ func IssueList(client *Client, ghRepo Repo, state string, labels []string, assig
|
|||
return nil, err
|
||||
}
|
||||
|
||||
issues := convertAPIToIssues(resp.Repository)
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
func convertAPIToIssues(i apiIssues) []Issue {
|
||||
var issues []Issue
|
||||
for _, edge := range i.Issues.Edges {
|
||||
var labels []string
|
||||
for _, labelEdge := range edge.Node.Labels.Edges {
|
||||
labels = append(labels, labelEdge.Node.Name)
|
||||
}
|
||||
|
||||
issue := Issue{
|
||||
Number: edge.Node.Number,
|
||||
Title: edge.Node.Title,
|
||||
URL: edge.Node.URL,
|
||||
Labels: labels,
|
||||
TotalLabelCount: edge.Node.Labels.TotalCount,
|
||||
}
|
||||
issues = append(issues, issue)
|
||||
}
|
||||
|
||||
return issues
|
||||
return resp.Repository.Issues.Nodes, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func randomString(length int) (string, error) {
|
||||
|
|
@ -101,11 +102,14 @@ func openInBrowser(url string) error {
|
|||
args = []string{"open"}
|
||||
case "windows":
|
||||
args = []string{"cmd", "/c", "start"}
|
||||
r := strings.NewReplacer("&", "^&")
|
||||
url = r.Replace(url)
|
||||
default:
|
||||
args = []string{"xdg-open"}
|
||||
}
|
||||
|
||||
args = append(args, url)
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
|
|
|||
194
command/issue.go
194
command/issue.go
|
|
@ -2,51 +2,43 @@ package command
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/github/gh-cli/api"
|
||||
"github.com/github/gh-cli/utils"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(issueCmd)
|
||||
issueCmd.AddCommand(
|
||||
&cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Show status of relevant issues",
|
||||
RunE: issueStatus,
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "view <issue-number>",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Short: "View an issue in the browser",
|
||||
RunE: issueView,
|
||||
},
|
||||
)
|
||||
issueCmd.AddCommand(issueCreateCmd)
|
||||
issueCreateCmd.Flags().StringArrayP("message", "m", nil, "set title and body")
|
||||
issueCreateCmd.Flags().BoolP("web", "w", false, "open the web browser to create an issue")
|
||||
issueCmd.AddCommand(issueStatusCmd)
|
||||
issueCmd.AddCommand(issueViewCmd)
|
||||
issueCreateCmd.Flags().StringP("title", "t", "",
|
||||
"Supply a title. Will prompt for one otherwise.")
|
||||
issueCreateCmd.Flags().StringP("body", "b", "",
|
||||
"Supply a body. Will prompt for one otherwise.")
|
||||
issueCreateCmd.Flags().BoolP("web", "w", false, "Open the web browser to create an issue")
|
||||
|
||||
issueListCmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List and filter issues in this repository",
|
||||
RunE: issueList,
|
||||
}
|
||||
issueListCmd.Flags().StringP("assignee", "a", "", "filter by assignee")
|
||||
issueListCmd.Flags().StringSliceP("label", "l", nil, "filter by label")
|
||||
issueListCmd.Flags().StringP("state", "s", "", "filter by state (open|closed|all)")
|
||||
issueListCmd.Flags().IntP("limit", "L", 30, "maximum number of issues to fetch")
|
||||
issueListCmd.Flags().StringP("assignee", "a", "", "Filter by assignee")
|
||||
issueListCmd.Flags().StringSliceP("label", "l", nil, "Filter by label")
|
||||
issueListCmd.Flags().StringP("state", "s", "", "Filter by state (open|closed|all)")
|
||||
issueListCmd.Flags().IntP("limit", "L", 30, "Maximum number of issues to fetch")
|
||||
issueCmd.AddCommand((issueListCmd))
|
||||
}
|
||||
|
||||
var issueCmd = &cobra.Command{
|
||||
Use: "issue",
|
||||
Short: "Work with issues",
|
||||
Short: "Create and view issues",
|
||||
Long: `Work with GitHub issues`,
|
||||
}
|
||||
var issueCreateCmd = &cobra.Command{
|
||||
|
|
@ -54,6 +46,17 @@ var issueCreateCmd = &cobra.Command{
|
|||
Short: "Create a new issue",
|
||||
RunE: issueCreate,
|
||||
}
|
||||
var issueStatusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Show status of relevant issues",
|
||||
RunE: issueStatus,
|
||||
}
|
||||
var issueViewCmd = &cobra.Command{
|
||||
Use: "view <issue-number>",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Short: "View an issue in the browser",
|
||||
RunE: issueView,
|
||||
}
|
||||
|
||||
func issueList(cmd *cobra.Command, args []string) error {
|
||||
ctx := contextForCommand(cmd)
|
||||
|
|
@ -92,12 +95,31 @@ func issueList(cmd *cobra.Command, args []string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if len(issues) > 0 {
|
||||
printIssues("", issues...)
|
||||
} else {
|
||||
message := fmt.Sprintf("There are no open issues")
|
||||
printMessage(message)
|
||||
out := cmd.OutOrStdout()
|
||||
colorOut := colorableOut(cmd)
|
||||
|
||||
if len(issues) == 0 {
|
||||
printMessage(colorOut, "There are no open issues")
|
||||
return nil
|
||||
}
|
||||
|
||||
table := utils.NewTablePrinter(out)
|
||||
for _, issue := range issues {
|
||||
issueNum := strconv.Itoa(issue.Number)
|
||||
if table.IsTTY() {
|
||||
issueNum = "#" + issueNum
|
||||
}
|
||||
labels := labelList(issue)
|
||||
if labels != "" && table.IsTTY() {
|
||||
labels = fmt.Sprintf("(%s)", labels)
|
||||
}
|
||||
table.AddField(issueNum, nil, colorFuncForState(issue.State))
|
||||
table.AddField(issue.Title, nil, nil)
|
||||
table.AddField(labels, nil, utils.Gray)
|
||||
table.EndRow()
|
||||
}
|
||||
table.Render()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -123,30 +145,32 @@ func issueStatus(cmd *cobra.Command, args []string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
printHeader("Issues assigned to you")
|
||||
out := colorableOut(cmd)
|
||||
|
||||
printHeader(out, "Issues assigned to you")
|
||||
if issuePayload.Assigned != nil {
|
||||
printIssues(" ", issuePayload.Assigned...)
|
||||
printIssues(out, " ", issuePayload.Assigned...)
|
||||
} else {
|
||||
message := fmt.Sprintf(" There are no issues assigned to you")
|
||||
printMessage(message)
|
||||
message := fmt.Sprintf(" There are no issues assgined to you")
|
||||
printMessage(out, message)
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Fprintln(out)
|
||||
|
||||
printHeader("Issues mentioning you")
|
||||
printHeader(out, "Issues mentioning you")
|
||||
if len(issuePayload.Mentioned) > 0 {
|
||||
printIssues(" ", issuePayload.Mentioned...)
|
||||
printIssues(out, " ", issuePayload.Mentioned...)
|
||||
} else {
|
||||
printMessage(" There are no issues mentioning you")
|
||||
printMessage(out, " There are no issues mentioning you")
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Fprintln(out)
|
||||
|
||||
printHeader("Recent issues")
|
||||
if len(issuePayload.Recent) > 0 {
|
||||
printIssues(" ", issuePayload.Recent...)
|
||||
printHeader(out, "Issues opened by you")
|
||||
if len(issuePayload.Authored) > 0 {
|
||||
printIssues(out, " ", issuePayload.Authored...)
|
||||
} else {
|
||||
printMessage(" There are no recent issues")
|
||||
printMessage(out, " There are no issues opened by you")
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Fprintln(out)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -189,44 +213,39 @@ func issueCreate(cmd *cobra.Command, args []string) error {
|
|||
return utils.OpenInBrowser(openURL)
|
||||
}
|
||||
|
||||
var title string
|
||||
var body string
|
||||
|
||||
message, err := cmd.Flags().GetStringArray("message")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiClient, err := apiClientForContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(message) > 0 {
|
||||
title = message[0]
|
||||
body = strings.Join(message[1:], "\n\n")
|
||||
} else {
|
||||
// TODO: open the text editor for issue title & body
|
||||
input := os.Stdin
|
||||
if terminal.IsTerminal(int(input.Fd())) {
|
||||
cmd.Println("Enter the issue title and body; press Enter + Ctrl-D when done:")
|
||||
}
|
||||
inputBytes, err := ioutil.ReadAll(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parts := strings.SplitN(string(inputBytes), "\n\n", 2)
|
||||
if len(parts) > 0 {
|
||||
title = parts[0]
|
||||
}
|
||||
if len(parts) > 1 {
|
||||
body = parts[1]
|
||||
}
|
||||
title, err := cmd.Flags().GetString("title")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not parse title")
|
||||
}
|
||||
body, err := cmd.Flags().GetString("body")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not parse body")
|
||||
}
|
||||
|
||||
if title == "" {
|
||||
return fmt.Errorf("aborting due to empty title")
|
||||
interactive := title == "" || body == ""
|
||||
|
||||
if interactive {
|
||||
tb, err := titleBodySurvey(cmd, title, body)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not collect title and/or body")
|
||||
}
|
||||
|
||||
if tb == nil {
|
||||
// editing was canceled, we can just leave
|
||||
return nil
|
||||
}
|
||||
|
||||
if title == "" {
|
||||
title = tb.Title
|
||||
}
|
||||
if body == "" {
|
||||
body = tb.Body
|
||||
}
|
||||
}
|
||||
params := map[string]interface{}{
|
||||
"title": title,
|
||||
|
|
@ -242,17 +261,30 @@ func issueCreate(cmd *cobra.Command, args []string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func printIssues(prefix string, issues ...api.Issue) {
|
||||
func printIssues(w io.Writer, prefix string, issues ...api.Issue) {
|
||||
for _, issue := range issues {
|
||||
number := utils.Green("#" + strconv.Itoa(issue.Number))
|
||||
var coloredLabels string
|
||||
if len(issue.Labels) > 0 {
|
||||
var ellipse string
|
||||
if issue.TotalLabelCount > len(issue.Labels) {
|
||||
ellipse = "…"
|
||||
}
|
||||
coloredLabels = utils.Gray(fmt.Sprintf(" (%s%s)", strings.Join(issue.Labels, ", "), ellipse))
|
||||
coloredLabels := labelList(issue)
|
||||
if coloredLabels != "" {
|
||||
coloredLabels = utils.Gray(fmt.Sprintf(" (%s)", coloredLabels))
|
||||
}
|
||||
fmt.Printf("%s%s %s %s\n", prefix, number, truncate(70, issue.Title), coloredLabels)
|
||||
fmt.Fprintf(w, "%s%s %s%s\n", prefix, number, truncate(70, issue.Title), coloredLabels)
|
||||
}
|
||||
}
|
||||
|
||||
func labelList(issue api.Issue) string {
|
||||
if len(issue.Labels.Nodes) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
labelNames := []string{}
|
||||
for _, label := range issue.Labels.Nodes {
|
||||
labelNames = append(labelNames, label.Name)
|
||||
}
|
||||
|
||||
list := strings.Join(labelNames, ", ")
|
||||
if issue.Labels.TotalCount > len(issue.Labels.Nodes) {
|
||||
list += ", …"
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,9 +55,9 @@ func TestIssueList(t *testing.T) {
|
|||
}
|
||||
|
||||
expectedIssues := []*regexp.Regexp{
|
||||
regexp.MustCompile(`#1.*won`),
|
||||
regexp.MustCompile(`#2.*too`),
|
||||
regexp.MustCompile(`#4.*fore`),
|
||||
regexp.MustCompile(`(?m)^1\t.*won`),
|
||||
regexp.MustCompile(`(?m)^2\t.*too`),
|
||||
regexp.MustCompile(`(?m)^4\t.*fore`),
|
||||
}
|
||||
|
||||
for _, r := range expectedIssues {
|
||||
|
|
@ -144,7 +144,7 @@ func TestIssueCreate(t *testing.T) {
|
|||
out := bytes.Buffer{}
|
||||
issueCreateCmd.SetOut(&out)
|
||||
|
||||
RootCmd.SetArgs([]string{"issue", "create", "-m", "hello", "-m", "ab", "-m", "cd"})
|
||||
RootCmd.SetArgs([]string{"issue", "create", "-t", "hello", "-b", "cash rules everything around me"})
|
||||
_, err := RootCmd.ExecuteC()
|
||||
if err != nil {
|
||||
t.Errorf("error running command `issue create`: %v", err)
|
||||
|
|
@ -164,7 +164,7 @@ func TestIssueCreate(t *testing.T) {
|
|||
|
||||
eq(t, reqBody.Variables.Input.RepositoryID, "REPOID")
|
||||
eq(t, reqBody.Variables.Input.Title, "hello")
|
||||
eq(t, reqBody.Variables.Input.Body, "ab\n\ncd")
|
||||
eq(t, reqBody.Variables.Input.Body, "cash rules everything around me")
|
||||
|
||||
eq(t, out.String(), "https://github.com/OWNER/REPO/issues/12\n")
|
||||
}
|
||||
|
|
|
|||
146
command/pr.go
146
command/pr.go
|
|
@ -2,6 +2,7 @@ package command
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
|
|
@ -10,7 +11,6 @@ import (
|
|||
"github.com/github/gh-cli/git"
|
||||
"github.com/github/gh-cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
|
@ -21,15 +21,15 @@ func init() {
|
|||
prCmd.AddCommand(prStatusCmd)
|
||||
prCmd.AddCommand(prViewCmd)
|
||||
|
||||
prListCmd.Flags().IntP("limit", "L", 30, "maximum number of items to fetch")
|
||||
prListCmd.Flags().StringP("state", "s", "open", "filter by state")
|
||||
prListCmd.Flags().StringP("base", "b", "", "filter by base branch")
|
||||
prListCmd.Flags().StringArrayP("label", "l", nil, "filter by label")
|
||||
prListCmd.Flags().IntP("limit", "L", 30, "Maximum number of items to fetch")
|
||||
prListCmd.Flags().StringP("state", "s", "open", "Filter by state")
|
||||
prListCmd.Flags().StringP("base", "B", "", "Filter by base branch")
|
||||
prListCmd.Flags().StringArrayP("label", "l", nil, "Filter by label")
|
||||
}
|
||||
|
||||
var prCmd = &cobra.Command{
|
||||
Use: "pr",
|
||||
Short: "Work with pull requests",
|
||||
Short: "Create, view, and checkout pull requests",
|
||||
Long: `Work with GitHub pull requests.`,
|
||||
}
|
||||
var prCheckoutCmd = &cobra.Command{
|
||||
|
|
@ -79,30 +79,32 @@ func prStatus(cmd *cobra.Command, args []string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
printHeader("Current branch")
|
||||
out := colorableOut(cmd)
|
||||
|
||||
printHeader(out, "Current branch")
|
||||
if prPayload.CurrentPR != nil {
|
||||
printPrs(*prPayload.CurrentPR)
|
||||
printPrs(out, *prPayload.CurrentPR)
|
||||
} else {
|
||||
message := fmt.Sprintf(" There is no pull request associated with %s", utils.Cyan("["+currentBranch+"]"))
|
||||
printMessage(message)
|
||||
printMessage(out, message)
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Fprintln(out)
|
||||
|
||||
printHeader("Created by you")
|
||||
printHeader(out, "Created by you")
|
||||
if len(prPayload.ViewerCreated) > 0 {
|
||||
printPrs(prPayload.ViewerCreated...)
|
||||
printPrs(out, prPayload.ViewerCreated...)
|
||||
} else {
|
||||
printMessage(" You have no open pull requests")
|
||||
printMessage(out, " You have no open pull requests")
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Fprintln(out)
|
||||
|
||||
printHeader("Requesting a code review from you")
|
||||
printHeader(out, "Requesting a code review from you")
|
||||
if len(prPayload.ReviewRequested) > 0 {
|
||||
printPrs(prPayload.ReviewRequested...)
|
||||
printPrs(out, prPayload.ReviewRequested...)
|
||||
} else {
|
||||
printMessage(" You have no pull requests to review")
|
||||
printMessage(out, " You have no pull requests to review")
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Fprintln(out)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -167,57 +169,38 @@ func prList(cmd *cobra.Command, args []string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
tty := false
|
||||
ttyWidth := 80
|
||||
out := cmd.OutOrStdout()
|
||||
if outFile, isFile := out.(*os.File); isFile {
|
||||
fd := int(outFile.Fd())
|
||||
tty = terminal.IsTerminal(fd)
|
||||
if w, _, err := terminal.GetSize(fd); err == nil {
|
||||
ttyWidth = w
|
||||
}
|
||||
}
|
||||
|
||||
numWidth := 0
|
||||
maxTitleWidth := 0
|
||||
table := utils.NewTablePrinter(cmd.OutOrStdout())
|
||||
for _, pr := range prs {
|
||||
numLen := len(strconv.Itoa(pr.Number)) + 1
|
||||
if numLen > numWidth {
|
||||
numWidth = numLen
|
||||
}
|
||||
if len(pr.Title) > maxTitleWidth {
|
||||
maxTitleWidth = len(pr.Title)
|
||||
prNum := strconv.Itoa(pr.Number)
|
||||
if table.IsTTY() {
|
||||
prNum = "#" + prNum
|
||||
}
|
||||
table.AddField(prNum, nil, colorFuncForState(pr.State))
|
||||
table.AddField(pr.Title, nil, nil)
|
||||
table.AddField(pr.HeadLabel(), nil, utils.Cyan)
|
||||
table.EndRow()
|
||||
}
|
||||
err = table.Render()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
branchWidth := 40
|
||||
titleWidth := ttyWidth - branchWidth - 2 - numWidth - 2
|
||||
|
||||
if maxTitleWidth < titleWidth {
|
||||
branchWidth += titleWidth - maxTitleWidth
|
||||
titleWidth = maxTitleWidth
|
||||
}
|
||||
|
||||
for _, pr := range prs {
|
||||
if tty {
|
||||
prNum := fmt.Sprintf("% *s", numWidth, fmt.Sprintf("#%d", pr.Number))
|
||||
switch pr.State {
|
||||
case "OPEN":
|
||||
prNum = utils.Green(prNum)
|
||||
case "CLOSED":
|
||||
prNum = utils.Red(prNum)
|
||||
case "MERGED":
|
||||
prNum = utils.Magenta(prNum)
|
||||
}
|
||||
prBranch := utils.Cyan(truncate(branchWidth, pr.HeadLabel()))
|
||||
fmt.Fprintf(out, "%s %-*s %s\n", prNum, titleWidth, truncate(titleWidth, pr.Title), prBranch)
|
||||
} else {
|
||||
fmt.Fprintf(out, "%d\t%s\t%s\n", pr.Number, pr.Title, pr.HeadLabel())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func colorFuncForState(state string) func(string) string {
|
||||
switch state {
|
||||
case "OPEN":
|
||||
return utils.Green
|
||||
case "CLOSED":
|
||||
return utils.Red
|
||||
case "MERGED":
|
||||
return utils.Magenta
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func prView(cmd *cobra.Command, args []string) error {
|
||||
ctx := contextForCommand(cmd)
|
||||
baseRepo, err := ctx.BaseRepo()
|
||||
|
|
@ -350,50 +333,51 @@ func prCheckout(cmd *cobra.Command, args []string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func printPrs(prs ...api.PullRequest) {
|
||||
func printPrs(w io.Writer, prs ...api.PullRequest) {
|
||||
for _, pr := range prs {
|
||||
prNumber := fmt.Sprintf("#%d", pr.Number)
|
||||
fmt.Printf(" %s %s %s", utils.Yellow(prNumber), truncate(50, pr.Title), utils.Cyan("["+pr.HeadLabel()+"]"))
|
||||
fmt.Fprintf(w, " %s %s %s", utils.Yellow(prNumber), truncate(50, pr.Title), utils.Cyan("["+pr.HeadLabel()+"]"))
|
||||
|
||||
checks := pr.ChecksStatus()
|
||||
reviews := pr.ReviewStatus()
|
||||
if checks.Total > 0 || reviews.ChangesRequested || reviews.Approved {
|
||||
fmt.Printf("\n ")
|
||||
fmt.Fprintf(w, "\n ")
|
||||
}
|
||||
|
||||
if checks.Total > 0 {
|
||||
var ratio string
|
||||
var summary string
|
||||
if checks.Failing > 0 {
|
||||
ratio = fmt.Sprintf("%d/%d", checks.Passing, checks.Total)
|
||||
ratio = utils.Red(ratio)
|
||||
if checks.Failing == checks.Total {
|
||||
summary = utils.Red("All checks failing")
|
||||
} else {
|
||||
summary = utils.Red(fmt.Sprintf("%d/%d checks failing", checks.Failing, checks.Total))
|
||||
}
|
||||
} else if checks.Pending > 0 {
|
||||
ratio = fmt.Sprintf("%d/%d", checks.Passing, checks.Total)
|
||||
ratio = utils.Yellow(ratio)
|
||||
summary = utils.Yellow("Checks pending")
|
||||
} else if checks.Passing == checks.Total {
|
||||
ratio = fmt.Sprintf("%d", checks.Total)
|
||||
ratio = utils.Green(ratio)
|
||||
summary = utils.Green("Checks passing")
|
||||
}
|
||||
fmt.Printf(" - checks: %s", ratio)
|
||||
fmt.Fprintf(w, " - %s", summary)
|
||||
}
|
||||
|
||||
if reviews.ChangesRequested {
|
||||
fmt.Printf(" - %s", utils.Red("changes requested"))
|
||||
fmt.Fprintf(w, " - %s", utils.Red("changes requested"))
|
||||
} else if reviews.ReviewRequired {
|
||||
fmt.Printf(" - %s", utils.Yellow("review required"))
|
||||
fmt.Fprintf(w, " - %s", utils.Yellow("review required"))
|
||||
} else if reviews.Approved {
|
||||
fmt.Printf(" - %s", utils.Green("approved"))
|
||||
fmt.Fprintf(w, " - %s", utils.Green("approved"))
|
||||
}
|
||||
|
||||
fmt.Printf("\n")
|
||||
fmt.Fprint(w, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
func printHeader(s string) {
|
||||
fmt.Println(utils.Bold(s))
|
||||
func printHeader(w io.Writer, s string) {
|
||||
fmt.Fprintln(w, utils.Bold(s))
|
||||
}
|
||||
|
||||
func printMessage(s string) {
|
||||
fmt.Println(utils.Gray(s))
|
||||
func printMessage(w io.Writer, s string) {
|
||||
fmt.Fprintln(w, utils.Gray(s))
|
||||
}
|
||||
|
||||
func truncate(maxLength int, title string) string {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import (
|
|||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/github/gh-cli/api"
|
||||
"github.com/github/gh-cli/context"
|
||||
"github.com/github/gh-cli/git"
|
||||
|
|
@ -55,86 +54,25 @@ func prCreate(cmd *cobra.Command, _ []string) error {
|
|||
|
||||
interactive := title == "" || body == ""
|
||||
|
||||
inProgress := struct {
|
||||
Body string
|
||||
Title string
|
||||
}{}
|
||||
|
||||
if interactive {
|
||||
confirmed := false
|
||||
editor := determineEditor()
|
||||
tb, err := titleBodySurvey(cmd, title, body)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not collect title and/or body")
|
||||
}
|
||||
|
||||
for !confirmed {
|
||||
titleQuestion := &survey.Question{
|
||||
Name: "title",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Pull request title",
|
||||
Default: inProgress.Title,
|
||||
},
|
||||
}
|
||||
bodyQuestion := &survey.Question{
|
||||
Name: "body",
|
||||
Prompt: &survey.Editor{
|
||||
Message: fmt.Sprintf("Pull request body (%s)", editor),
|
||||
FileName: "*.md",
|
||||
Default: inProgress.Body,
|
||||
AppendDefault: true,
|
||||
Editor: editor,
|
||||
},
|
||||
}
|
||||
if tb == nil {
|
||||
// editing was canceled, we can just leave
|
||||
return nil
|
||||
}
|
||||
|
||||
qs := []*survey.Question{}
|
||||
if title == "" {
|
||||
qs = append(qs, titleQuestion)
|
||||
}
|
||||
if body == "" {
|
||||
qs = append(qs, bodyQuestion)
|
||||
}
|
||||
|
||||
err := survey.Ask(qs, &inProgress)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not prompt")
|
||||
}
|
||||
confirmAnswers := struct {
|
||||
Confirmation string
|
||||
}{}
|
||||
confirmQs := []*survey.Question{
|
||||
{
|
||||
Name: "confirmation",
|
||||
Prompt: &survey.Select{
|
||||
Message: "Submit?",
|
||||
Options: []string{
|
||||
"Yes",
|
||||
"Edit",
|
||||
"Cancel",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = survey.Ask(confirmQs, &confirmAnswers)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not prompt")
|
||||
}
|
||||
|
||||
switch confirmAnswers.Confirmation {
|
||||
case "Yes":
|
||||
confirmed = true
|
||||
case "Edit":
|
||||
continue
|
||||
case "Cancel":
|
||||
cmd.Println("Discarding pull request")
|
||||
return nil
|
||||
}
|
||||
if title == "" {
|
||||
title = tb.Title
|
||||
}
|
||||
if body == "" {
|
||||
body = tb.Body
|
||||
}
|
||||
}
|
||||
|
||||
if title == "" {
|
||||
title = inProgress.Title
|
||||
}
|
||||
if body == "" {
|
||||
body = inProgress.Body
|
||||
}
|
||||
base, err := cmd.Flags().GetString("base")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not parse base")
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ func TestPRView_NoActiveBranch(t *testing.T) {
|
|||
})
|
||||
defer restoreCmd()
|
||||
|
||||
output, err := test.RunCommand(RootCmd, "pr view")
|
||||
_, err := test.RunCommand(RootCmd, "pr view")
|
||||
if err == nil || err.Error() != "the 'master' branch has no open pull requests" {
|
||||
t.Errorf("error running command `pr view`: %v", err)
|
||||
}
|
||||
|
|
@ -157,7 +157,7 @@ func TestPRView_NoActiveBranch(t *testing.T) {
|
|||
}
|
||||
|
||||
// Now run again but provide a PR number
|
||||
output, err = test.RunCommand(RootCmd, "pr view 23")
|
||||
output, err := test.RunCommand(RootCmd, "pr view 23")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `pr view`: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,13 @@ package command
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/github/gh-cli/api"
|
||||
"github.com/github/gh-cli/context"
|
||||
"github.com/github/gh-cli/utils"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
|
@ -17,9 +20,12 @@ var Version = "DEV"
|
|||
var BuildDate = "YYYY-MM-DD"
|
||||
|
||||
func init() {
|
||||
RootCmd.Version = fmt.Sprintf("%s (%s)", Version, BuildDate)
|
||||
RootCmd.PersistentFlags().StringP("repo", "R", "", "current GitHub repository")
|
||||
RootCmd.PersistentFlags().StringP("current-branch", "B", "", "current git branch")
|
||||
RootCmd.Version = fmt.Sprintf("%s (%s)", strings.TrimPrefix(Version, "v"), BuildDate)
|
||||
RootCmd.AddCommand(versionCmd)
|
||||
|
||||
RootCmd.PersistentFlags().StringP("repo", "R", "", "Current GitHub repository")
|
||||
RootCmd.PersistentFlags().Bool("help", false, "Show help for command")
|
||||
RootCmd.Flags().Bool("version", false, "Print gh version")
|
||||
// TODO:
|
||||
// RootCmd.PersistentFlags().BoolP("verbose", "V", false, "enable verbose output")
|
||||
|
||||
|
|
@ -37,12 +43,20 @@ type FlagError struct {
|
|||
var RootCmd = &cobra.Command{
|
||||
Use: "gh",
|
||||
Short: "GitHub CLI",
|
||||
Long: `Work with GitHub from your terminal`,
|
||||
Long: `Work seamlessly with GitHub from the command line`,
|
||||
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Hidden: true,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("gh version %s\n", RootCmd.Version)
|
||||
},
|
||||
}
|
||||
|
||||
// overriden in tests
|
||||
var initContext = func() context.Context {
|
||||
ctx := context.New()
|
||||
|
|
@ -56,9 +70,7 @@ func contextForCommand(cmd *cobra.Command) context.Context {
|
|||
ctx := initContext()
|
||||
if repo, err := cmd.Flags().GetString("repo"); err == nil && repo != "" {
|
||||
ctx.SetBaseRepo(repo)
|
||||
}
|
||||
if branch, err := cmd.Flags().GetString("current-branch"); err == nil && branch != "" {
|
||||
ctx.SetBranch(branch)
|
||||
ctx.SetBranch("master")
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
|
@ -82,3 +94,11 @@ var apiClientForContext = func(ctx context.Context) (*api.Client, error) {
|
|||
}
|
||||
return api.NewClient(opts...), nil
|
||||
}
|
||||
|
||||
func colorableOut(cmd *cobra.Command) io.Writer {
|
||||
out := cmd.OutOrStdout()
|
||||
if outFile, isFile := out.(*os.File); isFile {
|
||||
return utils.NewColorable(outFile)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
|
|
|||
104
command/title_body_survey.go
Normal file
104
command/title_body_survey.go
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type titleBody struct {
|
||||
Body string
|
||||
Title string
|
||||
}
|
||||
|
||||
const (
|
||||
_confirmed = iota
|
||||
_unconfirmed = iota
|
||||
_cancel = iota
|
||||
)
|
||||
|
||||
func confirm() (int, error) {
|
||||
confirmAnswers := struct {
|
||||
Confirmation int
|
||||
}{}
|
||||
confirmQs := []*survey.Question{
|
||||
{
|
||||
Name: "confirmation",
|
||||
Prompt: &survey.Select{
|
||||
Message: "Submit?",
|
||||
Options: []string{
|
||||
"Yes",
|
||||
"Edit",
|
||||
"Cancel",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := survey.Ask(confirmQs, &confirmAnswers)
|
||||
if err != nil {
|
||||
return -1, errors.Wrap(err, "could not prompt")
|
||||
}
|
||||
|
||||
return confirmAnswers.Confirmation, nil
|
||||
}
|
||||
|
||||
func titleBodySurvey(cmd *cobra.Command, providedTitle string, providedBody string) (*titleBody, error) {
|
||||
inProgress := titleBody{}
|
||||
|
||||
confirmed := false
|
||||
editor := determineEditor()
|
||||
|
||||
for !confirmed {
|
||||
titleQuestion := &survey.Question{
|
||||
Name: "title",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Title",
|
||||
Default: inProgress.Title,
|
||||
},
|
||||
}
|
||||
bodyQuestion := &survey.Question{
|
||||
Name: "body",
|
||||
Prompt: &survey.Editor{
|
||||
Message: fmt.Sprintf("Body (%s)", editor),
|
||||
FileName: "*.md",
|
||||
Default: inProgress.Body,
|
||||
AppendDefault: true,
|
||||
Editor: editor,
|
||||
},
|
||||
}
|
||||
|
||||
qs := []*survey.Question{}
|
||||
if providedTitle == "" {
|
||||
qs = append(qs, titleQuestion)
|
||||
}
|
||||
if providedBody == "" {
|
||||
qs = append(qs, bodyQuestion)
|
||||
}
|
||||
|
||||
err := survey.Ask(qs, &inProgress)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not prompt")
|
||||
}
|
||||
|
||||
confirmA, err := confirm()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to confirm")
|
||||
}
|
||||
switch confirmA {
|
||||
case _confirmed:
|
||||
confirmed = true
|
||||
case _unconfirmed:
|
||||
continue
|
||||
case _cancel:
|
||||
cmd.Println("Discarding.")
|
||||
return nil, nil
|
||||
default:
|
||||
panic("reached unreachable case")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return &inProgress, nil
|
||||
}
|
||||
|
|
@ -66,6 +66,9 @@ func setupConfigFile(filename string) (*configEntry, error) {
|
|||
defer config.Close()
|
||||
|
||||
yamlData, err := yaml.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n, err := config.Write(yamlData)
|
||||
if err == nil && n < len(yamlData) {
|
||||
err = io.ErrShortWrite
|
||||
|
|
|
|||
101
git/git.go
101
git/git.go
|
|
@ -4,7 +4,6 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
|
@ -31,16 +30,6 @@ func Dir() (string, error) {
|
|||
return gitDir, nil
|
||||
}
|
||||
|
||||
func WorkdirName() (string, error) {
|
||||
toplevelCmd := exec.Command("git", "rev-parse", "--show-toplevel")
|
||||
output, err := utils.PrepareCmd(toplevelCmd).Output()
|
||||
dir := firstLine(output)
|
||||
if dir == "" {
|
||||
return "", fmt.Errorf("unable to determine git working directory")
|
||||
}
|
||||
return dir, err
|
||||
}
|
||||
|
||||
func VerifyRef(ref string) bool {
|
||||
showRef := exec.Command("git", "show-ref", "--verify", "--quiet", ref)
|
||||
err := utils.PrepareCmd(showRef).Run()
|
||||
|
|
@ -73,72 +62,10 @@ func BranchAtRef(paths ...string) (name string, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func Editor() (string, error) {
|
||||
varCmd := exec.Command("git", "var", "GIT_EDITOR")
|
||||
output, err := utils.PrepareCmd(varCmd).Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Can't load git var: GIT_EDITOR")
|
||||
}
|
||||
|
||||
return os.ExpandEnv(firstLine(output)), nil
|
||||
}
|
||||
|
||||
func Head() (string, error) {
|
||||
return BranchAtRef("HEAD")
|
||||
}
|
||||
|
||||
func SymbolicFullName(name string) (string, error) {
|
||||
parseCmd := exec.Command("git", "rev-parse", "--symbolic-full-name", name)
|
||||
output, err := utils.PrepareCmd(parseCmd).Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Unknown revision or path not in the working tree: %s", name)
|
||||
}
|
||||
|
||||
return firstLine(output), nil
|
||||
}
|
||||
|
||||
func CommentChar(text string) (string, error) {
|
||||
char, err := Config("core.commentchar")
|
||||
if err != nil {
|
||||
return "#", nil
|
||||
} else if char == "auto" {
|
||||
lines := strings.Split(text, "\n")
|
||||
commentCharCandidates := strings.Split("#;@!$%^&|:", "")
|
||||
candidateLoop:
|
||||
for _, candidate := range commentCharCandidates {
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, candidate) {
|
||||
continue candidateLoop
|
||||
}
|
||||
}
|
||||
return candidate, nil
|
||||
}
|
||||
return "", fmt.Errorf("unable to select a comment character that is not used in the current message")
|
||||
} else {
|
||||
return char, nil
|
||||
}
|
||||
}
|
||||
|
||||
func Show(sha string) (string, error) {
|
||||
cmd := exec.Command("git", "-c", "log.showSignature=false", "show", "-s", "--format=%s%n%+b", sha)
|
||||
output, err := utils.PrepareCmd(cmd).Output()
|
||||
return strings.TrimSpace(string(output)), err
|
||||
}
|
||||
|
||||
func Log(sha1, sha2 string) (string, error) {
|
||||
shaRange := fmt.Sprintf("%s...%s", sha1, sha2)
|
||||
cmd := exec.Command(
|
||||
"-c", "log.showSignature=false", "log", "--no-color",
|
||||
"--format=%h (%aN, %ar)%n%w(78,3,3)%s%n%+b",
|
||||
"--cherry", shaRange)
|
||||
outputs, err := utils.PrepareCmd(cmd).Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Can't load git log %s..%s", sha1, sha2)
|
||||
}
|
||||
|
||||
return string(outputs), nil
|
||||
}
|
||||
|
||||
func listRemotes() ([]string, error) {
|
||||
remoteCmd := exec.Command("git", "remote", "-v")
|
||||
output, err := utils.PrepareCmd(remoteCmd).Output()
|
||||
|
|
@ -156,34 +83,6 @@ func Config(name string) (string, error) {
|
|||
|
||||
}
|
||||
|
||||
func ConfigAll(name string) ([]string, error) {
|
||||
mode := "--get-all"
|
||||
if strings.Contains(name, "*") {
|
||||
mode = "--get-regexp"
|
||||
}
|
||||
|
||||
configCmd := exec.Command("git", "config", mode, name)
|
||||
output, err := utils.PrepareCmd(configCmd).Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unknown config %s", name)
|
||||
}
|
||||
return outputLines(output), nil
|
||||
}
|
||||
|
||||
func LocalBranches() ([]string, error) {
|
||||
branchesCmd := exec.Command("git", "branch", "--list")
|
||||
output, err := utils.PrepareCmd(branchesCmd).Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
branches := []string{}
|
||||
for _, branch := range outputLines(output) {
|
||||
branches = append(branches, branch[2:])
|
||||
}
|
||||
return branches, nil
|
||||
}
|
||||
|
||||
var GitCommand = func(args ...string) *exec.Cmd {
|
||||
return exec.Command("git", args...)
|
||||
}
|
||||
|
|
|
|||
20
test/fixtures/issueList.json
vendored
20
test/fixtures/issueList.json
vendored
|
|
@ -2,57 +2,45 @@
|
|||
"data": {
|
||||
"repository": {
|
||||
"issues": {
|
||||
"edges": [
|
||||
"nodes": [
|
||||
{
|
||||
"node": {
|
||||
"number": 1,
|
||||
"title": "number won",
|
||||
"url": "https://wow.com",
|
||||
"labels": {
|
||||
"edges": [
|
||||
"nodes": [
|
||||
{
|
||||
"node": {
|
||||
"name": "label"
|
||||
}
|
||||
}
|
||||
],
|
||||
"totalCount": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"number": 2,
|
||||
"title": "number too",
|
||||
"url": "https://wow.com",
|
||||
"labels": {
|
||||
"edges": [
|
||||
"nodes": [
|
||||
{
|
||||
"node": {
|
||||
"name": "label"
|
||||
}
|
||||
}
|
||||
],
|
||||
"totalCount": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"number": 4,
|
||||
"title": "number fore",
|
||||
"url": "https://wow.com",
|
||||
"labels": {
|
||||
"edges": [
|
||||
"nodes": [
|
||||
{
|
||||
"node": {
|
||||
"name": "label"
|
||||
}
|
||||
}
|
||||
],
|
||||
"totalCount": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
16
test/fixtures/issueStatus.json
vendored
16
test/fixtures/issueStatus.json
vendored
|
|
@ -2,43 +2,35 @@
|
|||
"data": {
|
||||
"assigned": {
|
||||
"issues": {
|
||||
"edges": [
|
||||
"nodes": [
|
||||
{
|
||||
"node": {
|
||||
"number": 9,
|
||||
"title": "corey thinks squash tastes bad"
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"number": 10,
|
||||
"title": "broccoli is a superfood"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"mentioned": {
|
||||
"issues": {
|
||||
"edges": [
|
||||
"nodes": [
|
||||
{
|
||||
"node": {
|
||||
"number": 8,
|
||||
"title": "rabbits eat carrots"
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"number": 11,
|
||||
"title": "swiss chard is neutral"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"recent": {
|
||||
"authored": {
|
||||
"issues": {
|
||||
"edges": []
|
||||
"nodes": []
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
98
ui/ui.go
98
ui/ui.go
|
|
@ -1,98 +0,0 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/mattn/go-colorable"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
type UI interface {
|
||||
Print(a ...interface{}) (n int, err error)
|
||||
Printf(format string, a ...interface{}) (n int, err error)
|
||||
Println(a ...interface{}) (n int, err error)
|
||||
Errorf(format string, a ...interface{}) (n int, err error)
|
||||
Errorln(a ...interface{}) (n int, err error)
|
||||
}
|
||||
|
||||
var (
|
||||
Stdout = colorable.NewColorableStdout()
|
||||
Stderr = colorable.NewColorableStderr()
|
||||
Default UI = Console{Stdout: Stdout, Stderr: Stderr}
|
||||
)
|
||||
|
||||
func Print(a ...interface{}) (n int) {
|
||||
n, err := Default.Print(a...)
|
||||
if err != nil {
|
||||
// If something as basic as printing to stdout fails, just panic and exit
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Printf(format string, a ...interface{}) (n int) {
|
||||
n, err := Default.Printf(format, a...)
|
||||
if err != nil {
|
||||
// If something as basic as printing to stdout fails, just panic and exit
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Println(a ...interface{}) (n int) {
|
||||
n, err := Default.Println(a...)
|
||||
if err != nil {
|
||||
// If something as basic as printing to stdout fails, just panic and exit
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Errorf(format string, a ...interface{}) (n int) {
|
||||
n, err := Default.Errorf(format, a...)
|
||||
if err != nil {
|
||||
// If something as basic as printing to stderr fails, just panic and exit
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Errorln(a ...interface{}) (n int) {
|
||||
n, err := Default.Errorln(a...)
|
||||
if err != nil {
|
||||
// If something as basic as printing to stderr fails, just panic and exit
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func IsTerminal(f *os.File) bool {
|
||||
return terminal.IsTerminal(int(f.Fd()))
|
||||
}
|
||||
|
||||
type Console struct {
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
}
|
||||
|
||||
func (c Console) Print(a ...interface{}) (n int, err error) {
|
||||
return fmt.Fprint(c.Stdout, a...)
|
||||
}
|
||||
|
||||
func (c Console) Printf(format string, a ...interface{}) (n int, err error) {
|
||||
return fmt.Fprintf(c.Stdout, format, a...)
|
||||
}
|
||||
|
||||
func (c Console) Println(a ...interface{}) (n int, err error) {
|
||||
return fmt.Fprintln(c.Stdout, a...)
|
||||
}
|
||||
|
||||
func (c Console) Errorf(format string, a ...interface{}) (n int, err error) {
|
||||
return fmt.Fprintf(c.Stderr, format, a...)
|
||||
}
|
||||
|
||||
func (c Console) Errorln(a ...interface{}) (n int, err error) {
|
||||
return fmt.Fprintln(c.Stderr, a...)
|
||||
}
|
||||
|
|
@ -1,40 +1,61 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/mattn/go-colorable"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/mgutz/ansi"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
var _isStdoutTerminal = false
|
||||
var checkedTerminal = false
|
||||
|
||||
func isStdoutTerminal() bool {
|
||||
if !checkedTerminal {
|
||||
fd := os.Stdout.Fd()
|
||||
_isStdoutTerminal = isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd)
|
||||
checkedTerminal = true
|
||||
}
|
||||
return _isStdoutTerminal
|
||||
}
|
||||
|
||||
// NewColorable returns an output stream that handles ANSI color sequences on Windows
|
||||
func NewColorable(f *os.File) io.Writer {
|
||||
return colorable.NewColorable(f)
|
||||
}
|
||||
|
||||
func makeColorFunc(color string) func(string) string {
|
||||
cf := ansi.ColorFunc(color)
|
||||
return func(arg string) string {
|
||||
output := arg
|
||||
if terminal.IsTerminal(int(os.Stdout.Fd())) {
|
||||
output = ansi.Color(color+arg+ansi.Reset, "")
|
||||
if isStdoutTerminal() {
|
||||
return cf(arg)
|
||||
}
|
||||
|
||||
return output
|
||||
return arg
|
||||
}
|
||||
}
|
||||
|
||||
var Black = makeColorFunc(ansi.Black)
|
||||
var White = makeColorFunc(ansi.White)
|
||||
var Magenta = makeColorFunc(ansi.Magenta)
|
||||
var Cyan = makeColorFunc(ansi.Cyan)
|
||||
var Red = makeColorFunc(ansi.Red)
|
||||
var Yellow = makeColorFunc(ansi.Yellow)
|
||||
var Blue = makeColorFunc(ansi.Blue)
|
||||
var Green = makeColorFunc(ansi.Green)
|
||||
var Gray = makeColorFunc(ansi.LightBlack)
|
||||
// Magenta outputs ANSI color if stdout is a tty
|
||||
var Magenta = makeColorFunc("magenta")
|
||||
|
||||
func Bold(arg string) string {
|
||||
output := arg
|
||||
if terminal.IsTerminal(int(os.Stdout.Fd())) {
|
||||
// This is really annoying. If you just define Bold as ColorFunc("+b") it will properly bold but
|
||||
// will not use the default color, resulting in black and probably unreadable text. This forces
|
||||
// the default color before bolding.
|
||||
output = ansi.Color(ansi.DefaultFG+arg+ansi.Reset, "+b")
|
||||
}
|
||||
return output
|
||||
}
|
||||
// Cyan outputs ANSI color if stdout is a tty
|
||||
var Cyan = makeColorFunc("cyan")
|
||||
|
||||
// Red outputs ANSI color if stdout is a tty
|
||||
var Red = makeColorFunc("red")
|
||||
|
||||
// Yellow outputs ANSI color if stdout is a tty
|
||||
var Yellow = makeColorFunc("yellow")
|
||||
|
||||
// Blue outputs ANSI color if stdout is a tty
|
||||
var Blue = makeColorFunc("blue")
|
||||
|
||||
// Green outputs ANSI color if stdout is a tty
|
||||
var Green = makeColorFunc("green")
|
||||
|
||||
// Gray outputs ANSI color if stdout is a tty
|
||||
var Gray = makeColorFunc("black+h")
|
||||
|
||||
// Bold outputs ANSI color if stdout is a tty
|
||||
var Bold = makeColorFunc("default+b")
|
||||
|
|
|
|||
182
utils/table_printer.go
Normal file
182
utils/table_printer.go
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
type TablePrinter interface {
|
||||
IsTTY() bool
|
||||
AddField(string, func(int, string) string, func(string) string)
|
||||
EndRow()
|
||||
Render() error
|
||||
}
|
||||
|
||||
func NewTablePrinter(w io.Writer) TablePrinter {
|
||||
if outFile, isFile := w.(*os.File); isFile {
|
||||
isCygwin := isatty.IsCygwinTerminal(outFile.Fd())
|
||||
if isatty.IsTerminal(outFile.Fd()) || isCygwin {
|
||||
ttyWidth := 80
|
||||
if w, _, err := terminal.GetSize(int(outFile.Fd())); err == nil {
|
||||
ttyWidth = w
|
||||
} else if isCygwin {
|
||||
tputCmd := exec.Command("tput", "cols")
|
||||
tputCmd.Stdin = os.Stdin
|
||||
if out, err := tputCmd.Output(); err == nil {
|
||||
if w, err := strconv.Atoi(strings.TrimSpace(string(out))); err == nil {
|
||||
ttyWidth = w
|
||||
}
|
||||
}
|
||||
}
|
||||
return &ttyTablePrinter{
|
||||
out: NewColorable(outFile),
|
||||
maxWidth: ttyWidth,
|
||||
}
|
||||
}
|
||||
}
|
||||
return &tsvTablePrinter{
|
||||
out: w,
|
||||
}
|
||||
}
|
||||
|
||||
type tableField struct {
|
||||
Text string
|
||||
TruncateFunc func(int, string) string
|
||||
ColorFunc func(string) string
|
||||
}
|
||||
|
||||
type ttyTablePrinter struct {
|
||||
out io.Writer
|
||||
maxWidth int
|
||||
rows [][]tableField
|
||||
}
|
||||
|
||||
func (t ttyTablePrinter) IsTTY() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *ttyTablePrinter) AddField(text string, truncateFunc func(int, string) string, colorFunc func(string) string) {
|
||||
if truncateFunc == nil {
|
||||
truncateFunc = truncate
|
||||
}
|
||||
if t.rows == nil {
|
||||
t.rows = [][]tableField{[]tableField{}}
|
||||
}
|
||||
rowI := len(t.rows) - 1
|
||||
field := tableField{
|
||||
Text: text,
|
||||
TruncateFunc: truncateFunc,
|
||||
ColorFunc: colorFunc,
|
||||
}
|
||||
t.rows[rowI] = append(t.rows[rowI], field)
|
||||
}
|
||||
|
||||
func (t *ttyTablePrinter) EndRow() {
|
||||
t.rows = append(t.rows, []tableField{})
|
||||
}
|
||||
|
||||
func (t *ttyTablePrinter) Render() error {
|
||||
if len(t.rows) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
numCols := len(t.rows[0])
|
||||
colWidths := make([]int, numCols)
|
||||
// measure maximum content width per column
|
||||
for _, row := range t.rows {
|
||||
for col, field := range row {
|
||||
textLen := len(field.Text)
|
||||
if textLen > colWidths[col] {
|
||||
colWidths[col] = textLen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delim := " "
|
||||
availWidth := t.maxWidth - colWidths[0] - ((numCols - 1) * len(delim))
|
||||
// add extra space from columns that are already narrower than threshold
|
||||
for col := 1; col < numCols; col++ {
|
||||
availColWidth := availWidth / (numCols - 1)
|
||||
if extra := availColWidth - colWidths[col]; extra > 0 {
|
||||
availWidth += extra
|
||||
}
|
||||
}
|
||||
// cap all but first column to fit available terminal width
|
||||
// TODO: support weighted instead of even redistribution
|
||||
for col := 1; col < numCols; col++ {
|
||||
availColWidth := availWidth / (numCols - 1)
|
||||
if colWidths[col] > availColWidth {
|
||||
colWidths[col] = availColWidth
|
||||
}
|
||||
}
|
||||
|
||||
for _, row := range t.rows {
|
||||
for col, field := range row {
|
||||
if col > 0 {
|
||||
_, err := fmt.Fprint(t.out, delim)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
truncVal := field.TruncateFunc(colWidths[col], field.Text)
|
||||
if col < numCols-1 {
|
||||
// pad value with spaces on the right
|
||||
truncVal = fmt.Sprintf("%-*s", colWidths[col], truncVal)
|
||||
}
|
||||
if field.ColorFunc != nil {
|
||||
truncVal = field.ColorFunc(truncVal)
|
||||
}
|
||||
_, err := fmt.Fprint(t.out, truncVal)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(row) > 0 {
|
||||
_, err := fmt.Fprint(t.out, "\n")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type tsvTablePrinter struct {
|
||||
out io.Writer
|
||||
currentCol int
|
||||
}
|
||||
|
||||
func (t tsvTablePrinter) IsTTY() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *tsvTablePrinter) AddField(text string, _ func(int, string) string, _ func(string) string) {
|
||||
if t.currentCol > 0 {
|
||||
fmt.Fprint(t.out, "\t")
|
||||
}
|
||||
fmt.Fprint(t.out, text)
|
||||
t.currentCol++
|
||||
}
|
||||
|
||||
func (t *tsvTablePrinter) EndRow() {
|
||||
fmt.Fprint(t.out, "\n")
|
||||
t.currentCol = 0
|
||||
}
|
||||
|
||||
func (t *tsvTablePrinter) Render() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func truncate(maxLength int, title string) string {
|
||||
if len(title) > maxLength {
|
||||
return title[0:maxLength-3] + "..."
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
|
@ -2,31 +2,13 @@ package utils
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/github/gh-cli/ui"
|
||||
"github.com/kballard/go-shellquote"
|
||||
)
|
||||
|
||||
var timeNow = time.Now
|
||||
|
||||
func Check(err error) {
|
||||
if err != nil {
|
||||
ui.Errorln(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func ConcatPaths(paths ...string) string {
|
||||
return strings.Join(paths, "/")
|
||||
}
|
||||
|
||||
func OpenInBrowser(url string) error {
|
||||
browser := os.Getenv("BROWSER")
|
||||
if browser == "" {
|
||||
|
|
@ -69,58 +51,3 @@ func searchBrowserLauncher(goos string) (browser string) {
|
|||
|
||||
return browser
|
||||
}
|
||||
|
||||
func CommandPath(cmd string) (string, error) {
|
||||
if runtime.GOOS == "windows" {
|
||||
cmd = cmd + ".exe"
|
||||
}
|
||||
|
||||
path, err := exec.LookPath(cmd)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
path, err = filepath.Abs(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filepath.EvalSymlinks(path)
|
||||
}
|
||||
|
||||
func TimeAgo(t time.Time) string {
|
||||
duration := timeNow().Sub(t)
|
||||
minutes := duration.Minutes()
|
||||
hours := duration.Hours()
|
||||
days := hours / 24
|
||||
months := days / 30
|
||||
years := months / 12
|
||||
|
||||
var val int
|
||||
var unit string
|
||||
|
||||
if minutes < 1 {
|
||||
return "now"
|
||||
} else if hours < 1 {
|
||||
val = int(minutes)
|
||||
unit = "minute"
|
||||
} else if days < 1 {
|
||||
val = int(hours)
|
||||
unit = "hour"
|
||||
} else if months < 1 {
|
||||
val = int(days)
|
||||
unit = "day"
|
||||
} else if years < 1 {
|
||||
val = int(months)
|
||||
unit = "month"
|
||||
} else {
|
||||
val = int(years)
|
||||
unit = "year"
|
||||
}
|
||||
|
||||
var plural string
|
||||
if val > 1 {
|
||||
plural = "s"
|
||||
}
|
||||
return fmt.Sprintf("%d %s%s ago", val, unit, plural)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue