From 6fc03ff86e9c6888c9c36f6a3f24e07974236d14 Mon Sep 17 00:00:00 2001 From: Carlos A Becker Date: Thu, 26 Jan 2023 14:47:20 -0300 Subject: feat: improve gitlab/github readme url Signed-off-by: Carlos A Becker --- github.go | 30 +++++++---------------- gitlab.go | 38 ++++++++++------------------- main.go | 16 +++---------- url.go | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ url_test.go | 29 ++++++++++++++++++++++ 5 files changed, 134 insertions(+), 59 deletions(-) create mode 100644 url.go create mode 100644 url_test.go diff --git a/github.go b/github.go index d3d2084..6b169fa 100644 --- a/github.go +++ b/github.go @@ -3,40 +3,29 @@ package main import ( "encoding/json" "errors" + "fmt" "io" "net/http" "net/url" "strings" ) -// isGitHubURL tests a string to determine if it is a well-structured GitHub URL. -func isGitHubURL(s string) (string, bool) { - if strings.HasPrefix(s, "github.com/") { - s = "https://" + s - } - - u, err := url.ParseRequestURI(s) - if err != nil { - return "", false - } - - return u.String(), strings.ToLower(u.Host) == "github.com" -} - // findGitHubREADME tries to find the correct README filename in a repository using GitHub API. -func findGitHubREADME(s string) (*source, error) { - sSplit := strings.Split(s, "/") - owner, repo := sSplit[3], sSplit[4] +func findGitHubREADME(u *url.URL) (*source, error) { + owner, repo, ok := strings.Cut(strings.TrimPrefix(u.Path, "/"), "/") + if !ok { + return nil, fmt.Errorf("invalid url: %s", u.String()) + } type readme struct { DownloadURL string `json:"download_url"` } - apiURL := "https://api.github.com/repos/" + owner + "/" + repo + "/readme" + apiURL := fmt.Sprintf("https://api.%s/repos/%s/%s/readme", u.Hostname(), owner, repo) // nolint:bodyclose // it is closed on the caller - res, err := http.Get(apiURL) + res, err := http.Get(apiURL) // nolint: gosec if err != nil { return nil, err } @@ -47,8 +36,7 @@ func findGitHubREADME(s string) (*source, error) { } var result readme - jsonErr := json.Unmarshal(body, &result) - if jsonErr != nil { + if err := json.Unmarshal(body, &result); err != nil { return nil, err } diff --git a/gitlab.go b/gitlab.go index 81fcf84..05e1239 100644 --- a/gitlab.go +++ b/gitlab.go @@ -3,42 +3,31 @@ package main import ( "encoding/json" "errors" + "fmt" "io" "net/http" "net/url" "strings" ) -// isGitLabURL tests a string to determine if it is a well-structured GitLab URL. -func isGitLabURL(s string) (string, bool) { - if strings.HasPrefix(s, "gitlab.com/") { - s = "https://" + s - } - - u, err := url.ParseRequestURI(s) - if err != nil { - return "", false - } - - return u.String(), strings.ToLower(u.Host) == "gitlab.com" -} - // findGitLabREADME tries to find the correct README filename in a repository using GitLab API. -func findGitLabREADME(s string) (*source, error) { - sSplit := strings.Split(s, "/") - owner, repo := sSplit[3], sSplit[4] +func findGitLabREADME(u *url.URL) (*source, error) { + owner, repo, ok := strings.Cut(strings.TrimPrefix(u.Path, "/"), "/") + if !ok { + return nil, fmt.Errorf("invalid url: %s", u.String()) + } projectPath := url.QueryEscape(owner + "/" + repo) type readme struct { - ReadmeUrl string `json:"readme_url"` + ReadmeURL string `json:"readme_url"` } - apiURL := "https://gitlab.com/api/v4/projects/" + projectPath + apiURL := fmt.Sprintf("https://%s/api/v4/projects/%s", u.Hostname(), projectPath) // nolint:bodyclose // it is closed on the caller - res, err := http.Get(apiURL) + res, err := http.Get(apiURL) // nolint: gosec if err != nil { return nil, err } @@ -49,23 +38,22 @@ func findGitLabREADME(s string) (*source, error) { } var result readme - jsonErr := json.Unmarshal(body, &result) - if jsonErr != nil { + if err := json.Unmarshal(body, &result); err != nil { return nil, err } - readmeRawUrl := strings.Replace(result.ReadmeUrl, "blob", "raw", -1) + readmeRawURL := strings.Replace(result.ReadmeURL, "blob", "raw", -1) if res.StatusCode == http.StatusOK { // nolint:bodyclose // it is closed on the caller - resp, err := http.Get(readmeRawUrl) + resp, err := http.Get(readmeRawURL) // nolint: gosec if err != nil { return nil, err } if resp.StatusCode == http.StatusOK { - return &source{resp.Body, readmeRawUrl}, nil + return &source{resp.Body, readmeRawURL}, nil } } diff --git a/main.go b/main.go index 17d66e7..5a42f9b 100644 --- a/main.go +++ b/main.go @@ -62,19 +62,9 @@ func sourceFromArg(arg string) (*source, error) { } // a GitHub or GitLab URL (even without the protocol): - if u, ok := isGitHubURL(arg); ok { - src, err := findGitHubREADME(u) - if err != nil { - return nil, err - } - return src, nil - } - if u, ok := isGitLabURL(arg); ok { - src, err := findGitLabREADME(u) - if err != nil { - return nil, err - } - return src, nil + src, err := readmeURL(arg) + if src != nil || err != nil { + return src, err } // HTTP(S) URLs: diff --git a/url.go b/url.go new file mode 100644 index 0000000..43ca167 --- /dev/null +++ b/url.go @@ -0,0 +1,80 @@ +package main + +import ( + "net/url" + "strings" + "sync" +) + +const ( + protoGithub = "github://" + protoGitlab = "gitlab://" + protoHTTPS = "https://" +) + +var ( + githubURL *url.URL + gitlabURL *url.URL + urlsOnce sync.Once +) + +func init() { + urlsOnce.Do(func() { + githubURL, _ = url.Parse("https://github.com") + gitlabURL, _ = url.Parse("https://gitlab.com") + }) +} + +func readmeURL(path string) (*source, error) { + switch { + case strings.HasPrefix(path, protoGithub): + if u := githubReadmeURL(path); u != nil { + return readmeURL(u.String()) + } + return nil, nil + case strings.HasPrefix(path, protoGitlab): + if u := gitlabReadmeURL(path); u != nil { + return readmeURL(u.String()) + } + return nil, nil + } + + if !strings.HasPrefix(path, protoHTTPS) { + path = protoHTTPS + path + } + u, err := url.Parse(path) + if err != nil { + return nil, err + } + + switch { + case u.Hostname() == githubURL.Hostname(): + return findGitHubREADME(u) + case u.Hostname() == gitlabURL.Hostname(): + return findGitLabREADME(u) + } + + return nil, nil +} + +func githubReadmeURL(path string) *url.URL { + path = strings.TrimPrefix(path, protoGithub) + parts := strings.Split(path, "/") + if len(parts) != 2 { + // custom hostnames are not supported yet + return nil + } + u, _ := url.Parse(githubURL.String()) + return u.JoinPath(path) +} + +func gitlabReadmeURL(path string) *url.URL { + path = strings.TrimPrefix(path, protoGitlab) + parts := strings.Split(path, "/") + if len(parts) != 2 { + // custom hostnames are not supported yet + return nil + } + u, _ := url.Parse(gitlabURL.String()) + return u.JoinPath(path) +} diff --git a/url_test.go b/url_test.go new file mode 100644 index 0000000..02d1547 --- /dev/null +++ b/url_test.go @@ -0,0 +1,29 @@ +package main + +import "testing" + +func TestURLParser(t *testing.T) { + for path, url := range map[string]string{ + "github.com/charmbracelet/glow": "https://raw.githubusercontent.com/charmbracelet/glow/master/README.md", + "github://charmbracelet/glow": "https://raw.githubusercontent.com/charmbracelet/glow/master/README.md", + "github://caarlos0/dotfiles.fish": "https://raw.githubusercontent.com/caarlos0/dotfiles.fish/main/README.md", + "github://tj/git-extras": "https://raw.githubusercontent.com/tj/git-extras/master/Readme.md", + "https://github.com/goreleaser/nfpm": "https://raw.githubusercontent.com/goreleaser/nfpm/main/README.md", + "gitlab.com/caarlos0/test": "https://gitlab.com/caarlos0/test/-/raw/master/README.md", + "gitlab://caarlos0/test": "https://gitlab.com/caarlos0/test/-/raw/master/README.md", + "https://gitlab.com/terrakok/gitlab-client": "https://gitlab.com/terrakok/gitlab-client/-/raw/develop/Readme.md", + } { + t.Run(path, func(t *testing.T) { + got, err := readmeURL(path) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if got == nil { + t.Fatalf("should not be nil") + } + if url != got.URL { + t.Errorf("expected url for %s to be %s, was %s", path, url, got.URL) + } + }) + } +} -- cgit v1.2.3