Skip to content
This repository has been archived by the owner on Nov 22, 2022. It is now read-only.

Add project view command #778

Merged
merged 9 commits into from
Jul 4, 2021
21 changes: 21 additions & 0 deletions api/repository_files.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package api

import "github.com/xanzy/go-gitlab"

// GetFile retrieves a file from repository. Note that file content is Base64 encoded.
var GetFile = func(client *gitlab.Client, projectID interface{}, path string, ref string) (*gitlab.File, error) {
if client == nil {
client = apiClient.Lab()
}

fileOpts := &gitlab.GetFileOptions{
Ref: &ref,
}
file, _, err := client.RepositoryFiles.GetFile(projectID, path, fileOpts)

if err != nil {
return nil, err
}

return file, nil
}
2 changes: 2 additions & 0 deletions commands/project/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
repoCmdDelete "github.com/profclems/glab/commands/project/delete"
repoCmdFork "github.com/profclems/glab/commands/project/fork"
repoCmdSearch "github.com/profclems/glab/commands/project/search"
repoCmdView "github.com/profclems/glab/commands/project/view"

"github.com/spf13/cobra"
)
Expand All @@ -28,6 +29,7 @@ func NewCmdRepo(f *cmdutils.Factory) *cobra.Command {
repoCmd.AddCommand(repoCmdDelete.NewCmdDelete(f))
repoCmd.AddCommand(repoCmdFork.NewCmdFork(f, nil))
repoCmd.AddCommand(repoCmdSearch.NewCmdSearch(f))
repoCmd.AddCommand(repoCmdView.NewCmdView(f))

return repoCmd
}
245 changes: 245 additions & 0 deletions commands/project/view/project_view.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
package view

import (
"encoding/base64"
"fmt"
"strings"

"github.com/MakeNowJust/heredoc"
"github.com/profclems/glab/api"
"github.com/profclems/glab/commands/cmdutils"
"github.com/profclems/glab/internal/glrepo"
"github.com/profclems/glab/pkg/iostreams"
"github.com/profclems/glab/pkg/utils"
"github.com/spf13/cobra"
"github.com/xanzy/go-gitlab"
)

type ViewOptions struct {
ProjectID string
APIClient *gitlab.Client
Web bool
Branch string
Browser string
GlamourStyle string

IO *iostreams.IOStreams
Repo glrepo.Interface
}

func NewCmdView(f *cmdutils.Factory) *cobra.Command {
opts := ViewOptions{
IO: f.IO,
}

var projectViewCmd = &cobra.Command{
Use: "view [repository] [flags]",
Short: "View a project/repository",
Long: `Display the description and README of a project or open it in the browser.`,
Args: cobra.MaximumNArgs(1),
Example: heredoc.Doc(`
# view project information for the current directory
$ glab repo view

# view project information of specified name
$ glab repo view my-project
$ glab repo view user/repo
$ glab repo view group/namespace/repo

# specify repo by full [git] URL
$ glab repo view [email protected]:user/repo.git
$ glab repo view https://gitlab.company.org/user/repo
$ glab repo view https://gitlab.company.org/user/repo.git
`),
RunE: func(cmd *cobra.Command, args []string) error {
var err error

cfg, err := f.Config()
if err != nil {
return err
}

if len(args) == 1 {
opts.ProjectID = args[0]
}

apiClient, err := f.HttpClient()
if err != nil {
return err
}
opts.APIClient = apiClient

if opts.ProjectID == "" {
opts.Repo, err = f.BaseRepo()
if err != nil {
return cmdutils.WrapError(err, "`repository` is required when not running in a git repository")
}
opts.ProjectID = opts.Repo.FullName()
}

if opts.ProjectID != "" {
if !strings.Contains(opts.ProjectID, "/") {
currentUser, err := api.CurrentUser(opts.APIClient)
if err != nil {
return cmdutils.WrapError(err, "Failed to retrieve your current user")
}

opts.ProjectID = currentUser.Username + "/" + opts.ProjectID
}

repo, err := glrepo.FromFullName(opts.ProjectID)
if err != nil {
return err
}

if !glrepo.IsSame(repo, opts.Repo) {
client, err := api.NewClientWithCfg(repo.RepoHost(), cfg, false)
if err != nil {
return err
}
opts.APIClient = client.Lab()
}
opts.Repo = repo
opts.ProjectID = repo.FullName()
}

if opts.Branch == "" {
opts.Branch, _ = f.Branch()
}

browser, _ := cfg.Get(opts.Repo.RepoHost(), "browser")
opts.Browser = browser

opts.GlamourStyle, _ = cfg.Get(opts.Repo.RepoHost(), "glamour_style")

return runViewProject(&opts)
},
}

projectViewCmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "Open a project in the browser")
projectViewCmd.Flags().StringVarP(&opts.Branch, "branch", "b", "", "View a specific branch of the repository")

return projectViewCmd
}

func runViewProject(opts *ViewOptions) error {
project, err := opts.Repo.Project(opts.APIClient)
if err != nil {
return cmdutils.WrapError(err, "Failed to retrieve project information")
}

readmeFile, err := getReadmeFile(opts, project)
if err != nil {
return err
}

if opts.Web {
openURL := generateProjectURL(project, opts.Branch)

if opts.IO.IsaTTY {
fmt.Fprintf(opts.IO.StdOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
}

return utils.OpenInBrowser(openURL, opts.Browser)
} else {
if opts.IO.IsaTTY {
brodock marked this conversation as resolved.
Show resolved Hide resolved
if err := opts.IO.StartPager(); err != nil {
return err
}
defer opts.IO.StopPager()

printProjectContentTTY(opts, project, readmeFile)
} else {
printProjectContentRaw(opts, project, readmeFile)
}
}

return nil
}

func getReadmeFile(opts *ViewOptions, project *gitlab.Project) (*gitlab.File, error) {
if project.ReadmeURL == "" {
return nil, nil
}

readmePath := strings.Replace(project.ReadmeURL, project.WebURL+"/-/blob/", "", 1)
readmePathComponents := strings.Split(readmePath, "/")
readmeRef := readmePathComponents[0]
readmeFileName := readmePathComponents[1]
readmeFile, err := api.GetFile(opts.APIClient, project.PathWithNamespace, readmeFileName, readmeRef)

if err != nil {
return nil, cmdutils.WrapError(err, "Failed to retrieve README file")
}

decoded, err := base64.StdEncoding.DecodeString(readmeFile.Content)
if err != nil {
return nil, cmdutils.WrapError(err, "Failed to decode README file")
}

readmeFile.Content = string(decoded)

return readmeFile, nil
}

func generateProjectURL(project *gitlab.Project, branch string) string {
if project.DefaultBranch != branch {
return project.WebURL + "/-/tree/" + branch
}

return project.WebURL
}

func printProjectContentTTY(opts *ViewOptions, project *gitlab.Project, readme *gitlab.File) {
var description string
var readmeContent string
var err error

fullName := project.NameWithNamespace
if project.Description != "" {
description, err = utils.RenderMarkdownWithoutIndentations(project.Description, opts.GlamourStyle)

if err != nil {
description = project.Description
}
} else {
description = "\n(No description provided)\n\n"
}

if readme != nil {
readmeContent, err = utils.RenderMarkdown(readme.Content, opts.GlamourStyle)

if err != nil {
readmeContent = readme.Content
}
}

c := opts.IO.Color()
// Header
fmt.Fprint(opts.IO.StdOut, c.Bold(fullName))
fmt.Fprint(opts.IO.StdOut, c.Gray(description))

// Readme
if readme != nil {
fmt.Fprint(opts.IO.StdOut, readmeContent)
} else {
fmt.Fprintln(opts.IO.StdOut, c.Gray("(This repository does not have a README file)"))
}

fmt.Fprintln(opts.IO.StdOut)
fmt.Fprintf(opts.IO.StdOut, c.Gray("View this project on GitLab: %s\n"), project.WebURL)
}

func printProjectContentRaw(opts *ViewOptions, project *gitlab.Project, readme *gitlab.File) {
fullName := project.NameWithNamespace
description := project.Description

fmt.Fprintf(opts.IO.StdOut, "name:\t%s\n", fullName)
fmt.Fprintf(opts.IO.StdOut, "description:\t%s\n", description)

if readme != nil {
fmt.Fprintln(opts.IO.StdOut, "---")
fmt.Fprintf(opts.IO.StdOut, readme.Content)
fmt.Fprintln(opts.IO.StdOut)
}
}
6 changes: 4 additions & 2 deletions internal/glrepo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,10 @@ func normalizeHostname(h string) string {

// IsSame compares two GitLab repositories
func IsSame(a, b Interface) bool {
return strings.EqualFold(a.RepoOwner(), b.RepoOwner()) &&
strings.EqualFold(a.RepoName(), b.RepoName()) &&
if a == nil || b == nil {
return false
}
return strings.EqualFold(a.FullName(), b.FullName()) &&
normalizeHostname(a.RepoHost()) == normalizeHostname(b.RepoHost())
}

Expand Down
37 changes: 34 additions & 3 deletions pkg/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"github.com/profclems/glab/pkg/browser"
)

type MarkdownRenderOpts []glamour.TermRendererOption

// OpenInBrowser opens the url in a web browser based on OS and $BROWSER environment variable
func OpenInBrowser(url, browserType string) error {
browseCmd, err := browser.Command(url, browserType)
Expand All @@ -21,20 +23,49 @@ func OpenInBrowser(url, browserType string) error {
}

func RenderMarkdown(text, glamourStyle string) (string, error) {
opts := MarkdownRenderOpts{
glamour.WithStylePath(getStyle(glamourStyle)),
}

return renderMarkdown(text, opts)
}

func RenderMarkdownWithoutIndentations(text, glamourStyle string) (string, error) {
opts := MarkdownRenderOpts{
glamour.WithStylePath(getStyle(glamourStyle)),
markdownWithoutIndentation(),
}

return renderMarkdown(text, opts)
}

func renderMarkdown(text string, opts MarkdownRenderOpts) (string, error) {
// Glamour rendering preserves carriage return characters in code blocks, but
// we need to ensure that no such characters are present in the output.
text = strings.ReplaceAll(text, "\r\n", "\n")

tr, err := glamour.NewTermRenderer(
glamour.WithStylePath(getStyle(glamourStyle)),
)
tr, err := glamour.NewTermRenderer(opts...)
if err != nil {
return "", err
}

return tr.Render(text)
}

func markdownWithoutIndentation() glamour.TermRendererOption {
overrides := []byte(`
{
"document": {
"margin": 0
},
"code_block": {
"margin": 0
}
}`)

return glamour.WithStylesFromJSONBytes(overrides)
}

func getStyle(glamourStyle string) string {
if glamourStyle == "" || glamourStyle == "none" {
return "notty"
Expand Down