refactor: enhance error handling

This commit is contained in:
Clement Sam 2020-08-15 08:39:03 +00:00
parent 4306c23cea
commit 09ba1378f4
29 changed files with 525 additions and 96 deletions

View File

@ -20,9 +20,9 @@ GO_LDFLAGS := -X main.version=$(GLAB_VERSION) $(GO_LDFLAGS)
GO_LDFLAGS := -X main.build=$(BUILD_DATE) $(GO_LDFLAGS)
build:
go build -trimpath -ldflags "$(GO_LDFLAGS)" -o ./bin/glab ./cmd/glab
go build -trimpath -ldflags "$(GO_LDFLAGS) -X main.usageMode=prod" -o ./bin/glab ./cmd/glab
run:
go run -trimpath -ldflags "$(GO_LDFLAGS)" ./cmd/glab $(var)
go run -trimpath -ldflags "$(GO_LDFLAGS) -X main.usageMode=dev" ./cmd/glab $(var)
test:
go test ./...
rt: #Test release

View File

@ -1,22 +1,86 @@
package main
import (
"errors"
"fmt"
"glab/commands"
"glab/internal/utils"
"io"
"net"
"os"
"regexp"
"strings"
"github.com/spf13/cobra"
"glab/commands"
"glab/internal/config"
)
// Version is set at build
var version string
// build is set at build
var build string
// usage mode is set at build to either "dev" or "prod" depending how binary is created
var usageMode string
var debug bool
func main() {
commands.Version = version
commands.Build = build
if err := commands.Execute(); err != nil {
fmt.Println(err)
initConfig()
if usageMode == "dev" {
debug = true
}
if cmd, err := commands.Execute(); err != nil {
printError(os.Stderr, err, cmd, debug)
os.Exit(1)
}
}
func initConfig() {
config.SetGlobalPathDir()
config.UseGlobalConfig = true
if config.GetEnv("GITLAB_URI") == "NOTFOUND" || config.GetEnv("GITLAB_URI") == "OK" {
config.SetEnv("GITLAB_URI", "https://gitlab.com")
}
if config.GetEnv("GIT_REMOTE_URL_VAR") == "NOTFOUND" || config.GetEnv("GIT_REMOTE_URL_VAR") == "OK" {
config.SetEnv("GIT_REMOTE_URL_VAR", "origin")
}
config.UseGlobalConfig = false
}
func printError(out io.Writer, err error, cmd *cobra.Command, debug bool) {
if err == utils.SilentError {
return
}
var dnsError *net.DNSError
if errors.As(err, &dnsError) {
_, _ = fmt.Fprintf(out, "error connecting to %s\n", dnsError.Name)
if debug {
_, _ = fmt.Fprintln(out, dnsError)
}
_, _ = fmt.Fprintln(out, "check your internet connection or status.gitlab.com or 'Run sudo gitlab-ctl status' on your server if self-hosted")
return
}
re := regexp.MustCompile(`(?s){(.*)}`)
m := re.FindAllStringSubmatch(err.Error(), -1)
if len(m) != 0 {
if len(m[0]) >= 1 {
_, _ = fmt.Fprintln(out, m[0][1])
}
} else {
_, _ = fmt.Fprintln(out, err)
}
var flagError *utils.FlagError
if errors.As(err, &flagError) || strings.HasPrefix(err.Error(), "unknown command ") {
if !strings.HasSuffix(err.Error(), "\n") {
_, _ = fmt.Fprintln(out)
}
_, _ = fmt.Fprintln(out, cmd.UsageString())
}
}

90
cmd/glab/main_test.go Normal file
View File

@ -0,0 +1,90 @@
package main
import (
"bytes"
"flag"
"fmt"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"glab/internal/config"
"glab/internal/utils"
"math/rand"
"net"
"strconv"
"testing"
"time"
)
// Test started when the test binary is started
// and calls the main function
func TestGlab(t *testing.T) {
rand.Seed(time.Now().UnixNano())
flag.Set("test", "../coverage-" + strconv.Itoa(int(rand.Uint64())) + ".out")
main()
}
func Test_printError(t *testing.T) {
cmd := &cobra.Command{}
type args struct {
err error
cmd *cobra.Command
debug bool
}
tests := []struct {
name string
args args
wantOut string
}{
{
name: "generic error",
args: args{
err: errors.New("the app exploded"),
cmd: nil,
debug: false,
},
wantOut: "the app exploded\n",
},
{
name: "DNS error",
args: args{
err: fmt.Errorf("DNS oopsie: %w", &net.DNSError{
Name: config.GetEnv("GITLAB_URI")+"/api/v4",
}),
cmd: nil,
debug: false,
},
wantOut: `error connecting to `+config.GetEnv("GITLAB_URI")+`/api/v4
check your internet connection or status.gitlab.com or 'Run sudo gitlab-ctl status' on your server if self-hosted
`,
},
{
name: "Cobra flag error",
args: args{
err: &utils.FlagError{Err: errors.New("unknown flag --foo")},
cmd: cmd,
debug: false,
},
wantOut: "unknown flag --foo\n\nUsage:\n\n",
},
{
name: "unknown Cobra command error",
args: args{
err: errors.New("unknown command foo"),
cmd: cmd,
debug: false,
},
wantOut: "unknown command foo\n\nUsage:\n\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
out := &bytes.Buffer{}
printError(out, tt.args.err, tt.args.cmd, tt.args.debug)
if gotOut := out.String(); gotOut != tt.wantOut {
t.Errorf("printError() = %q, want %q", gotOut, tt.wantOut)
}
})
}
}

View File

@ -6,7 +6,6 @@ import (
"github.com/xanzy/go-gitlab"
"glab/internal/git"
"glab/internal/manip"
"log"
"strings"
)
@ -16,10 +15,10 @@ var issueCloseCmd = &cobra.Command{
Long: ``,
Aliases: []string{"unsub"},
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 1 {
cmdErr(cmd, args)
return
return nil
}
if len(args) > 0 {
issueID := strings.TrimSpace(args[0])
@ -34,7 +33,7 @@ var issueCloseCmd = &cobra.Command{
fmt.Println("Closing Issue...")
issue, resp, err := gitlabClient.Issues.UpdateIssue(repo, manip.StringToInt(i2), l)
if err != nil {
log.Fatal(err)
return err
}
if isSuccessful(resp.StatusCode) {
fmt.Println("Issue #" + i2 + " closed")
@ -47,7 +46,9 @@ var issueCloseCmd = &cobra.Command{
}
} else {
cmdErr(cmd, args)
return nil
}
return nil
},
}

View File

@ -1,7 +1,6 @@
package commands
import (
"log"
"strings"
"github.com/spf13/cobra"
@ -16,10 +15,10 @@ var issueCreateCmd = &cobra.Command{
Long: ``,
Aliases: []string{"new"},
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
cmdErr(cmd, args)
return
return nil
}
l := &gitlab.CreateIssueOptions{}
@ -31,11 +30,6 @@ var issueCreateCmd = &cobra.Command{
} else {
issueTitle = manip.AskQuestionWithInput("Title", "", true)
}
if label, _ := cmd.Flags().GetString("label"); label != "" {
issueLabel = strings.Trim(label, "[] ")
} else {
issueLabel = manip.AskQuestionWithInput("Label(s) [Comma Separated]", "", false)
}
if description, _ := cmd.Flags().GetString("description"); description != "" {
issueDescription = strings.Trim(description, " ")
} else {
@ -49,6 +43,11 @@ var issueCreateCmd = &cobra.Command{
})
}
}
if label, _ := cmd.Flags().GetString("label"); label != "" {
issueLabel = strings.Trim(label, "[] ")
} else {
issueLabel = manip.AskQuestionWithInput("Label(s) [Comma Separated]", "", false)
}
l.Title = gitlab.String(issueTitle)
l.Labels = &gitlab.Labels{issueLabel}
l.Description = &issueDescription
@ -81,9 +80,10 @@ var issueCreateCmd = &cobra.Command{
}
issue, _, err := gitlabClient.Issues.CreateIssue(repo, l)
if err != nil {
log.Fatal(err)
return err
}
displayIssue(issue)
return nil
},
}

View File

@ -14,10 +14,10 @@ var issueDeleteCmd = &cobra.Command{
Long: ``,
Aliases: []string{"del"},
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 1 {
cmdErr(cmd, args)
return
return nil
}
if len(args) > 0 {
issueID := strings.TrimSpace(args[0])
@ -40,6 +40,7 @@ var issueDeleteCmd = &cobra.Command{
} else {
cmdErr(cmd, args)
}
return nil
},
}

View File

@ -4,7 +4,6 @@ import (
"github.com/spf13/cobra"
"github.com/xanzy/go-gitlab"
"glab/internal/git"
"log"
)
var issueListCmd = &cobra.Command{
@ -13,7 +12,7 @@ var issueListCmd = &cobra.Command{
Long: ``,
Aliases: []string{"ls"},
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
var state string
if lb, _ := cmd.Flags().GetBool("all"); lb {
state = "all"
@ -45,9 +44,10 @@ var issueListCmd = &cobra.Command{
}
issues, _, err := gitlabClient.Issues.ListProjectIssues(repo, l)
if err != nil {
log.Fatal(err)
return err
}
displayAllIssues(issues)
return nil
},
}

View File

@ -1,13 +1,12 @@
package commands
import (
"errors"
"fmt"
"glab/internal/git"
"glab/internal/manip"
"log"
"github.com/spf13/cobra"
gitlab "github.com/xanzy/go-gitlab"
"glab/internal/git"
"glab/internal/manip"
)
var issueNoteCreateCmd = &cobra.Command{
@ -16,7 +15,7 @@ var issueNoteCreateCmd = &cobra.Command{
Short: "Add a comment or note to an issue on Gitlab",
Long: ``,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
gitlabClient, repo := git.InitGitlabClient()
mID := args[0]
@ -25,13 +24,11 @@ var issueNoteCreateCmd = &cobra.Command{
repo = r
}
if err != nil {
er(err)
return
return err
}
mr, _, err := gitlabClient.Issues.GetIssue(repo, manip.StringToInt(mID))
if err != nil {
er(err)
return
return err
}
if body == "" {
body = manip.Editor(manip.EditorOptions{
@ -41,16 +38,17 @@ var issueNoteCreateCmd = &cobra.Command{
})
}
if body == "" {
log.Fatal("Aborted... Note is empty")
return errors.New("aborted... Note is empty")
}
noteInfo,_, err := gitlabClient.Notes.CreateIssueNote(repo, manip.StringToInt(mID), &gitlab.CreateIssueNoteOptions{
Body: &body,
})
if err != nil {
log.Fatal(err)
return err
}
fmt.Printf("%s#note_%d\n",mr.WebURL, noteInfo.ID)
return nil
},
}

View File

@ -6,7 +6,6 @@ import (
"github.com/xanzy/go-gitlab"
"glab/internal/git"
"glab/internal/manip"
"log"
"strings"
)
@ -16,10 +15,10 @@ var issueReopenCmd = &cobra.Command{
Long: ``,
Aliases: []string{"open"},
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 1 {
cmdErr(cmd, args)
return
return nil
}
if len(args) > 0 {
issueID := strings.TrimSpace(args[0])
@ -34,7 +33,7 @@ var issueReopenCmd = &cobra.Command{
fmt.Println("Reopening Issue...")
issue, resp, err := gitlabClient.Issues.UpdateIssue(repo, manip.StringToInt(i2), l)
if err != nil {
log.Fatal(err)
return err
}
if isSuccessful(resp.StatusCode) {
fmt.Println("Issue #" + i2 + " eopened")
@ -48,6 +47,7 @@ var issueReopenCmd = &cobra.Command{
} else {
cmdErr(cmd, args)
}
return nil
},
}

View File

@ -14,10 +14,10 @@ var issueSubscribeCmd = &cobra.Command{
Long: ``,
Aliases: []string{"sub"},
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 1 {
cmdErr(cmd, args)
return
return nil
}
if len(args) > 0 {
mergeID := strings.TrimSpace(args[0])
@ -42,6 +42,7 @@ var issueSubscribeCmd = &cobra.Command{
} else {
cmdErr(cmd, args)
}
return nil
},
}

15
commands/issue_test.go Normal file
View File

@ -0,0 +1,15 @@
package commands
import (
"testing"
test "github.com/smartystreets/goconvey/convey"
)
func TestIssueCmd(t *testing.T) {
test.Convey("test issue", t, func() {
args := []string{"issue"}
RootCmd.SetArgs(args)
test.ShouldBeNil(RootCmd.Execute())
})
}

View File

@ -14,10 +14,10 @@ var issueUnsubscribeCmd = &cobra.Command{
Long: ``,
Aliases: []string{"unsub"},
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 1 {
cmdErr(cmd, args)
return
return nil
}
if len(args) > 0 {
mergeID := strings.TrimSpace(args[0])
@ -28,8 +28,10 @@ var issueUnsubscribeCmd = &cobra.Command{
arrIds := strings.Split(strings.Trim(mergeID, "[] "), ",")
for _, i2 := range arrIds {
fmt.Println("Unsubscribing to Issue #" + i2)
issue, resp, _ := gitlabClient.Issues.UnsubscribeFromIssue(repo, manip.StringToInt(i2))
issue, resp, err := gitlabClient.Issues.UnsubscribeFromIssue(repo, manip.StringToInt(i2))
if err != nil {
return nil
}
if isSuccessful(resp.StatusCode) {
fmt.Println("Unsubscribed to issue #" + i2)
displayIssue(issue)
@ -42,6 +44,7 @@ var issueUnsubscribeCmd = &cobra.Command{
} else {
cmdErr(cmd, args)
}
return nil
},
}

View File

@ -11,7 +11,6 @@ import (
"glab/internal/git"
"glab/internal/manip"
"glab/internal/utils"
"log"
"strings"
"time"
)
@ -22,10 +21,10 @@ var issueViewCmd = &cobra.Command{
Long: ``,
Aliases: []string{"show"},
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 || len(args) > 1 {
cmdErr(cmd, args)
return
return nil
}
pid := manip.StringToInt(args[0])
@ -37,17 +36,17 @@ var issueViewCmd = &cobra.Command{
issue, _, err := gitlabClient.Issues.GetIssue(repo, pid)
if err != nil {
log.Fatal(err)
return err
}
if lb, _ := cmd.Flags().GetBool("web"); lb { //open in browser if --web flag is specified
a, err := browser.Command(issue.WebURL)
if err != nil {
er(err)
return err
}
if err := a.Run(); err != nil {
er(err)
return err
}
return
return nil
}
var issueState string
if issue.State == "opened" {
@ -108,7 +107,7 @@ var issueViewCmd = &cobra.Command{
l := &gitlab.ListIssueNotesOptions{}
notes, _, err := gitlabClient.Notes.ListIssueNotes(repo, pid, l)
if err != nil {
er(err)
return err
}
table := uitable.New()
@ -138,6 +137,7 @@ var issueViewCmd = &cobra.Command{
fmt.Println("There are no comments on this issue")
}
}
return nil
},
}

View File

@ -7,11 +7,15 @@ var labelCmd = &cobra.Command{
Use: "label <command> [flags]",
Short: `Manage labels on remote`,
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 || len(args) > 2 {
cmd.Help()
return
err := cmd.Help()
if err != nil {
return err
}
return nil
}
return nil
},
}

15
commands/label_test.go Normal file
View File

@ -0,0 +1,15 @@
package commands
import (
"testing"
test "github.com/smartystreets/goconvey/convey"
)
func TestLabelCmd(t *testing.T) {
test.Convey("test label cmd", t, func() {
args := []string{"label"}
RootCmd.SetArgs(args)
test.ShouldBeNil(RootCmd.Execute())
})
}

View File

@ -16,7 +16,7 @@ var mrCreateNoteCmd = &cobra.Command{
Short: "Add a comment or note to merge request",
Long: ``,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
gitlabClient, repo := git.InitGitlabClient()
mID := args[0]
@ -25,13 +25,11 @@ var mrCreateNoteCmd = &cobra.Command{
repo = r
}
if err != nil {
er(err)
return
return err
}
mr, _, err := gitlabClient.MergeRequests.GetMergeRequest(repo, manip.StringToInt(mID), &gitlab.GetMergeRequestsOptions{})
if err != nil {
er(err)
return
return err
}
if body == "" {
body = manip.Editor(manip.EditorOptions{
@ -48,9 +46,10 @@ var mrCreateNoteCmd = &cobra.Command{
Body: &body,
})
if err != nil {
log.Fatal(err)
return err
}
fmt.Printf("%s#note_%d\n",mr.WebURL, noteInfo.ID)
return nil
},
}

15
commands/mr_test.go Normal file
View File

@ -0,0 +1,15 @@
package commands
import (
"testing"
test "github.com/smartystreets/goconvey/convey"
)
func TestMrCmd(t *testing.T) {
test.Convey("test mr", t, func() {
args := []string{"mr"}
RootCmd.SetArgs(args)
RootCmd.Execute()
})
}

View File

@ -293,7 +293,7 @@ func pipelineJobTraceWithSha(pid interface{}, sha, name string) (io.Reader, *git
return r, job, err
}
// CIJobs returns a list of jobs in a pipeline for a given sha. The jobs are
// pipelineJobsWithSha returns a list of jobs in a pipeline for a given sha. The jobs are
// returned sorted by their CreatedAt time
func pipelineJobsWithSha(pid interface{}, sha string) ([]*gitlab.Job, error) {
gitlabClient, _ := git.InitGitlabClient()

19
commands/pipeline_test.go Normal file
View File

@ -0,0 +1,19 @@
package commands
import (
test "github.com/smartystreets/goconvey/convey"
"testing"
)
func TestPipelineCmd(t *testing.T) {
test.Convey("test pipeline cmd", t, func() {
args := []string{"pipeline"}
RootCmd.SetArgs(args)
test.ShouldBeNil(RootCmd.Execute())
})
test.Convey("test pipeline alias cmd", t, func() {
args := []string{"pipe"}
RootCmd.SetArgs(args)
test.ShouldBeNil(RootCmd.Execute())
})
}

View File

@ -55,9 +55,9 @@ var RootCmd = &cobra.Command{
}
// Execute executes the root command.
func Execute() error {
func Execute() (*cobra.Command, error) {
RootCmd.Flags().BoolP("version", "v", false, "show glab version information")
return RootCmd.Execute()
return RootCmd.ExecuteC()
}
// versionCmd represents the version command
@ -79,7 +79,6 @@ var configCmd = &cobra.Command{
}
func init() {
cobra.OnInitialize(initConfig)
RootCmd.AddCommand(updateCmd)
initConfigCmd()
RootCmd.AddCommand(configCmd)
@ -94,18 +93,6 @@ func cmdErr(cmd *cobra.Command, args []string) {
_ = cmd.Usage()
}
func initConfig() {
config.SetGlobalPathDir()
config.UseGlobalConfig = true
if config.GetEnv("GITLAB_URI") == "NOTFOUND" || config.GetEnv("GITLAB_URI") == "OK" {
config.SetEnv("GITLAB_URI", "https://gitlab.com")
}
if config.GetEnv("GIT_REMOTE_URL_VAR") == "NOTFOUND" || config.GetEnv("GIT_REMOTE_URL_VAR") == "OK" {
config.SetEnv("GIT_REMOTE_URL_VAR", "origin")
}
config.UseGlobalConfig = false
}
func initConfigCmd() {
configCmd.Flags().BoolP("global", "g", false, "Set configuration globally")
configCmd.Flags().StringP("url", "u", "", "specify the url of the gitlab server if self hosted (eg: https://gitlab.example.com).")

44
commands/user.go Normal file
View File

@ -0,0 +1,44 @@
package commands
import (
"log"
"github.com/xanzy/go-gitlab"
"glab/internal/git"
)
func currentUser(token string) (string, error) {
gLab, _ := git.InitGitlabClient()
u, _, err := gLab.Users.CurrentUser()
if err != nil {
return "", err
}
return u.Username, nil
}
func getUser(uid int) (*gitlab.User, error) {
gLab, _ := git.InitGitlabClient()
u, _, err := gLab.Users.GetUser(uid)
if err != nil {
return nil, err
}
return u, nil
}
func getUsername(uid int) string {
u, err := getUser(uid)
if err != nil {
log.Fatal(err)
}
return u.Username
}
func getUserActivities() ([]*gitlab.UserActivity, error) {
gLab, _ := git.InitGitlabClient()
l := &gitlab.GetUserActivitiesOptions{}
ua, _, err := gLab.Users.GetUserActivities(l)
if err != nil {
return nil, err
}
return ua, nil
}

15
commands/version_test.go Normal file
View File

@ -0,0 +1,15 @@
package commands
import (
"testing"
test "github.com/smartystreets/goconvey/convey"
)
func TestVersionCmd(t *testing.T) {
test.Convey("test version cmd", t, func() {
args := []string{"version"}
RootCmd.SetArgs(args)
test.ShouldBeNil(RootCmd.Execute())
})
}

1
go.mod
View File

@ -21,6 +21,7 @@ require (
github.com/onsi/gomega v1.10.1 // indirect
github.com/pkg/errors v0.9.1
github.com/rivo/tview v0.0.0-20200712113419-c65badfc3d92
github.com/smartystreets/goconvey v1.6.4
github.com/spf13/cobra v1.0.0
github.com/spf13/pflag v1.0.5 // indirect
github.com/tcnksm/go-gitconfig v0.1.2

11
go.sum
View File

@ -26,6 +26,7 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/charmbracelet/glamour v0.2.0 h1:mTgaiNiumpqTZp3qVM6DH9UB0NlbY17wejoMf1kM8Pg=
github.com/charmbracelet/glamour v0.2.0/go.mod h1:UA27Kwj3QHialP74iU6C+Gpc8Y7IOAKupeKMLLBURWM=
github.com/cli/cli v0.11.1 h1:NqGtJSScC6vqfgMdplu5dBRME9pcActcYSFfcQbBa3k=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
@ -89,6 +90,8 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/gookit/color v1.2.7 h1:4qePMNWZhrmbfYJDix+J4V2l0iVW+6jQGjicELlN14E=
github.com/gookit/color v1.2.7/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gosuri/uilive v0.0.4 h1:hUEBpQDj8D8jXgtCdBu7sWsy5sbW/5GhuO8KBwJ2jyY=
github.com/gosuri/uilive v0.0.4/go.mod h1:V/epo5LjjlDE5RJUcqx8dbw+zc93y5Ya3yg8tfZ74VI=
@ -111,6 +114,8 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
@ -206,6 +211,10 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
@ -317,6 +326,8 @@ golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b h1:mSUCVIwDx4hfXJfWsOPfdzEHxzb2Xjl6BQ8YgPnazQA=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -4,10 +4,6 @@ import (
"bytes"
"errors"
"fmt"
"github.com/tcnksm/go-gitconfig"
"github.com/xanzy/go-gitlab"
"glab/internal/config"
"glab/internal/run"
"log"
"net/url"
"os"
@ -15,6 +11,11 @@ import (
"path"
"regexp"
"strings"
"github.com/tcnksm/go-gitconfig"
"github.com/xanzy/go-gitlab"
"glab/internal/config"
"glab/internal/run"
)
// GetRepo returns the repo name of the git directory with the namespace like profclems/glab
@ -378,6 +379,107 @@ func firstLine(output []byte) string {
return string(output)
}
var remoteRE = regexp.MustCompile(`(.+)\s+(.+)\s+\((push|fetch)\)`)
// RemoteSet is a slice of git remotes
type RemoteSet []*Remote
func NewRemote(name string, u string) *Remote {
pu, _ := url.Parse(u)
return &Remote{
Name: name,
FetchURL: pu,
PushURL: pu,
}
}
// Remote is a parsed git remote
type Remote struct {
Name string
FetchURL *url.URL
PushURL *url.URL
}
func (r *Remote) String() string {
return r.Name
}
// Remotes gets the git remotes set for the current repo
func Remotes() (RemoteSet, error) {
list, err := listRemotes()
if err != nil {
return nil, err
}
return parseRemotes(list), nil
}
func parseRemotes(gitRemotes []string) (remotes RemoteSet) {
for _, r := range gitRemotes {
match := remoteRE.FindStringSubmatch(r)
if match == nil {
continue
}
name := strings.TrimSpace(match[1])
urlStr := strings.TrimSpace(match[2])
urlType := strings.TrimSpace(match[3])
var rem *Remote
if len(remotes) > 0 {
rem = remotes[len(remotes)-1]
if name != rem.Name {
rem = nil
}
}
if rem == nil {
rem = &Remote{Name: name}
remotes = append(remotes, rem)
}
u, err := ParseURL(urlStr)
if err != nil {
continue
}
switch urlType {
case "fetch":
rem.FetchURL = u
case "push":
rem.PushURL = u
}
}
return
}
// AddRemote adds a new git remote and auto-fetches objects from it
func AddRemote(name, u string) (*Remote, error) {
addCmd := exec.Command("git", "remote", "add", "-f", name, u)
err := run.PrepareCmd(addCmd).Run()
if err != nil {
return nil, err
}
var urlParsed *url.URL
if strings.HasPrefix(u, "https") {
urlParsed, err = url.Parse(u)
if err != nil {
return nil, err
}
} else {
urlParsed, err = ParseURL(u)
if err != nil {
return nil, err
}
}
return &Remote{
Name: name,
FetchURL: urlParsed,
PushURL: urlParsed,
}, nil
}
func RunCmd(args []string) (err error) {
gitCmd := GitCommand(args...)
gitCmd.Stdin = os.Stdin

36
internal/git/git_test.go Normal file
View File

@ -0,0 +1,36 @@
package git
import (
"reflect"
"testing"
)
func TestCommits(t *testing.T) {
type args struct {
baseRef string
headRef string
}
tests := []struct {
name string
args args
want []*Commit
wantErr bool
}{
{
name: "Commit",
args: args{"trunk","HEAD"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Commits(tt.args.baseRef, tt.args.headRef)
if (err != nil) != tt.wantErr {
t.Errorf("Commits() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Commits() got = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -44,17 +44,6 @@ func ParseURL(rawURL string) (u *url.URL, err error) {
}
// IsValidUrl tests a string to determine if it is a well-structured url or not.
func IsValidURL(toTest string) bool {
_, err := url.ParseRequestURI(toTest)
if err != nil {
if strings.HasPrefix(toTest, "git@") {
return true
}
if strings.HasPrefix(toTest, "ssh:") {
return true
}
return false
}
return true
func IsValidURL(uri string) bool {
return strings.HasPrefix(uri, "git@") || protocolRe.MatchString(uri)
}

View File

@ -15,7 +15,7 @@ func TestIsValidURL(t *testing.T) {
{"Group namespace", args{"group/namespace/repo"}, false},
{"HTTPS Protocol", args{"https://gitlab.com/profclems/glab.git"}, true},
{"With SSH", args{"git@gitlab.com:profclems/glab.git"}, true},
{"SSH Protocol", args{"ssh:user@example.com:my-project"}, true},
{"SSH Protocol", args{"ssh://user@example.com:my-project"}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

19
internal/utils/errors.go Normal file
View File

@ -0,0 +1,19 @@
package utils
import "errors"
// FlagError is the kind of error raised in flag processing
type FlagError struct {
Err error
}
func (fe FlagError) Error() string {
return fe.Err.Error()
}
func (fe FlagError) Unwrap() error {
return fe.Err
}
// SilentError is an error that triggers exit code 1 without any error messaging
var SilentError = errors.New("SilentError")