feat(server): add tree & blob server commands

Ayman Bagabas created

Replace show & list

Change summary

git/repo.go        |  23 ++++++----
server/cmd/blob.go | 107 ++++++++++++++++++++++++++++++++---------------
server/cmd/cmd.go  |   3 
server/cmd/list.go |  79 +++--------------------------------
server/cmd/tree.go |  99 ++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 195 insertions(+), 116 deletions(-)

Detailed changes

git/repo.go 🔗

@@ -114,6 +114,19 @@ func (r *Repository) References() ([]*Reference, error) {
 	return rrefs, nil
 }
 
+// LsTree returns the tree for the given reference.
+func (r *Repository) LsTree(ref string) (*Tree, error) {
+	tree, err := r.Repository.LsTree(ref)
+	if err != nil {
+		return nil, err
+	}
+	return &Tree{
+		Tree:       tree,
+		Path:       "",
+		Repository: r,
+	}, nil
+}
+
 // Tree returns the tree for the given reference.
 func (r *Repository) Tree(ref *Reference) (*Tree, error) {
 	if ref == nil {
@@ -123,15 +136,7 @@ func (r *Repository) Tree(ref *Reference) (*Tree, error) {
 		}
 		ref = rref
 	}
-	tree, err := r.LsTree(ref.Hash.String())
-	if err != nil {
-		return nil, err
-	}
-	return &Tree{
-		Tree:       tree,
-		Path:       "",
-		Repository: r,
-	}, nil
+	return r.LsTree(ref.Hash.String())
 }
 
 // TreePath returns the tree for the given path.

server/cmd/show.go → server/cmd/blob.go 🔗

@@ -7,7 +7,7 @@ import (
 	"github.com/alecthomas/chroma/lexers"
 	gansi "github.com/charmbracelet/glamour/ansi"
 	"github.com/charmbracelet/lipgloss"
-	"github.com/charmbracelet/soft-serve/server/backend"
+	"github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/ui/common"
 	"github.com/muesli/termenv"
 	"github.com/spf13/cobra"
@@ -21,60 +21,99 @@ var (
 	filemodeStyle  = lipgloss.NewStyle().Foreground(lipgloss.Color("#777777"))
 )
 
-// showCommand returns a command that prints the contents of a file.
-func showCommand() *cobra.Command {
+// blobCommand returns a command that prints the contents of a file.
+func blobCommand() *cobra.Command {
 	var linenumber bool
 	var color bool
+	var raw bool
 
-	showCmd := &cobra.Command{
-		Use:               "show PATH",
-		Aliases:           []string{"cat"},
-		Short:             "Read the contents of file at path.",
-		Args:              cobra.ExactArgs(1),
+	cmd := &cobra.Command{
+		Use:               "blob REPOSITORY [REFERENCE] [PATH]",
+		Aliases:           []string{"cat", "show"},
+		Short:             "Print out the contents of file at path.",
+		Args:              cobra.RangeArgs(1, 3),
 		PersistentPreRunE: checkIfReadable,
 		RunE: func(cmd *cobra.Command, args []string) error {
 			cfg, _ := fromContext(cmd)
-			// FIXME: nested repos are not supported.
-			ps := strings.Split(args[0], "/")
-			rn := strings.TrimSuffix(ps[0], ".git")
-			fp := strings.Join(ps[1:], "/")
-			var repo backend.Repository
-			repoExists := false
-			repos, err := cfg.Backend.Repositories()
+			rn := args[0]
+			ref := ""
+			fp := ""
+			switch len(args) {
+			case 2:
+				fp = args[1]
+			case 3:
+				ref = args[1]
+				fp = args[2]
+			}
+
+			repo, err := cfg.Backend.Repository(rn)
 			if err != nil {
 				return err
 			}
-			for _, rp := range repos {
-				if rp.Name() == rn {
-					repoExists = true
-					repo = rp
-					break
-				}
-			}
-			if !repoExists {
-				return ErrRepoNotFound
-			}
-			c, _, err := backend.LatestFile(repo, fp)
+
+			r, err := repo.Open()
 			if err != nil {
 				return err
 			}
-			if color {
-				c, err = withFormatting(fp, c)
+
+			if ref == "" {
+				head, err := r.HEAD()
 				if err != nil {
 					return err
 				}
+				ref = head.Hash.String()
+			}
+
+			tree, err := r.LsTree(ref)
+			if err != nil {
+				return err
+			}
+
+			te, err := tree.TreeEntry(fp)
+			if err != nil {
+				return err
+			}
+
+			if te.Type() != "blob" {
+				return git.ErrFileNotFound
+			}
+
+			bts, err := te.Contents()
+			if err != nil {
+				return err
 			}
-			if linenumber {
-				c = withLineNumber(c, color)
+
+			c := string(bts)
+			isBin, _ := te.File().IsBinary()
+			if isBin {
+				if raw {
+					cmd.Println(c)
+				} else {
+					return fmt.Errorf("binary file: use --raw to print")
+				}
+			} else {
+				if color {
+					c, err = withFormatting(fp, c)
+					if err != nil {
+						return err
+					}
+				}
+
+				if linenumber {
+					c = withLineNumber(c, color)
+				}
+
+				cmd.Println(c)
 			}
-			cmd.Println(c)
 			return nil
 		},
 	}
-	showCmd.Flags().BoolVarP(&linenumber, "linenumber", "l", false, "Print line numbers")
-	showCmd.Flags().BoolVarP(&color, "color", "c", false, "Colorize output")
 
-	return showCmd
+	cmd.Flags().BoolVarP(&raw, "raw", "r", false, "Print raw contents")
+	cmd.Flags().BoolVarP(&linenumber, "linenumber", "l", false, "Print line numbers")
+	cmd.Flags().BoolVarP(&color, "color", "c", false, "Colorize output")
+
+	return cmd
 }
 
 func withLineNumber(s string, color bool) string {

server/cmd/cmd.go 🔗

@@ -55,8 +55,9 @@ func rootCommand() *cobra.Command {
 		listCommand(),
 		privateCommand(),
 		renameCommand(),
-		showCommand(),
+		blobCommand(),
 		tagCommand(),
+		treeCommand(),
 	)
 
 	return rootCmd

server/cmd/list.go 🔗

@@ -1,11 +1,6 @@
 package cmd
 
 import (
-	"fmt"
-	"path/filepath"
-	"strings"
-
-	"github.com/charmbracelet/soft-serve/git"
 	"github.com/charmbracelet/soft-serve/server/backend"
 	"github.com/spf13/cobra"
 )
@@ -13,80 +8,20 @@ import (
 // listCommand returns a command that list file or directory at path.
 func listCommand() *cobra.Command {
 	listCmd := &cobra.Command{
-		Use:     "list PATH",
+		Use:     "list",
 		Aliases: []string{"ls"},
-		Short:   "List files at repository.",
-		Args:    cobra.RangeArgs(0, 1),
+		Short:   "List repositories.",
+		Args:    cobra.NoArgs,
 		RunE: func(cmd *cobra.Command, args []string) error {
 			cfg, s := fromContext(cmd)
-			rn := ""
-			path := ""
-			ps := []string{}
-			if len(args) > 0 {
-				// FIXME: nested repos are not supported.
-				path = filepath.Clean(args[0])
-				ps = strings.Split(path, "/")
-				rn = strings.TrimSuffix(ps[0], ".git")
-				auth := cfg.Backend.AccessLevel(rn, s.PublicKey())
-				if auth < backend.ReadOnlyAccess {
-					return ErrUnauthorized
-				}
-			}
-			if path == "" || path == "." || path == "/" {
-				repos, err := cfg.Backend.Repositories()
-				if err != nil {
-					return err
-				}
-				for _, r := range repos {
-					if cfg.Backend.AccessLevel(r.Name(), s.PublicKey()) >= backend.ReadOnlyAccess {
-						cmd.Println(r.Name())
-					}
-				}
-				return nil
-			}
-			rr, err := cfg.Backend.Repository(rn)
-			if err != nil {
-				return err
-			}
-			r, err := rr.Open()
-			if err != nil {
-				return err
-			}
-			head, err := r.HEAD()
-			if err != nil {
-				if bs, err := r.Branches(); err != nil && len(bs) == 0 {
-					return fmt.Errorf("repository is empty")
-				}
-				return err
-			}
-			tree, err := r.TreePath(head, "")
+			repos, err := cfg.Backend.Repositories()
 			if err != nil {
 				return err
 			}
-			subpath := strings.Join(ps[1:], "/")
-			ents := git.Entries{}
-			te, err := tree.TreeEntry(subpath)
-			if err == git.ErrRevisionNotExist {
-				return ErrFileNotFound
-			}
-			if err != nil {
-				return err
-			}
-			if te.Type() == "tree" {
-				tree, err = tree.SubTree(subpath)
-				if err != nil {
-					return err
+			for _, r := range repos {
+				if cfg.Backend.AccessLevel(r.Name(), s.PublicKey()) >= backend.ReadOnlyAccess {
+					cmd.Println(r.Name())
 				}
-				ents, err = tree.Entries()
-				if err != nil {
-					return err
-				}
-			} else {
-				ents = append(ents, te)
-			}
-			ents.Sort()
-			for _, ent := range ents {
-				cmd.Printf("%s\t%d\t %s\n", ent.Mode(), ent.Size(), ent.Name())
 			}
 			return nil
 		},

server/cmd/tree.go 🔗

@@ -0,0 +1,99 @@
+package cmd
+
+import (
+	"fmt"
+
+	"github.com/charmbracelet/soft-serve/git"
+	"github.com/dustin/go-humanize"
+	"github.com/spf13/cobra"
+)
+
+// treeCommand returns a command that list file or directory at path.
+func treeCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:               "tree REPOSITORY [REFERENCE] [PATH]",
+		Short:             "Print repository tree at path.",
+		Args:              cobra.RangeArgs(1, 3),
+		PersistentPreRunE: checkIfReadable,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			cfg, _ := fromContext(cmd)
+			rn := args[0]
+			path := ""
+			ref := ""
+			switch len(args) {
+			case 2:
+				path = args[1]
+			case 3:
+				ref = args[1]
+				path = args[2]
+			}
+			rr, err := cfg.Backend.Repository(rn)
+			if err != nil {
+				return err
+			}
+
+			r, err := rr.Open()
+			if err != nil {
+				return err
+			}
+
+			if ref == "" {
+				head, err := r.HEAD()
+				if err != nil {
+					if bs, err := r.Branches(); err != nil && len(bs) == 0 {
+						return fmt.Errorf("repository is empty")
+					}
+					return err
+				}
+
+				ref = head.Hash.String()
+			}
+
+			tree, err := r.LsTree(ref)
+			if err != nil {
+				return err
+			}
+
+			ents := git.Entries{}
+			if path != "" && path != "/" {
+				te, err := tree.TreeEntry(path)
+				if err == git.ErrRevisionNotExist {
+					return ErrFileNotFound
+				}
+				if err != nil {
+					return err
+				}
+				if te.Type() == "tree" {
+					tree, err = tree.SubTree(path)
+					if err != nil {
+						return err
+					}
+					ents, err = tree.Entries()
+					if err != nil {
+						return err
+					}
+				} else {
+					ents = append(ents, te)
+				}
+			} else {
+				ents, err = tree.Entries()
+				if err != nil {
+					return err
+				}
+			}
+			ents.Sort()
+			for _, ent := range ents {
+				size := ent.Size()
+				ssize := ""
+				if size == 0 {
+					ssize = "-"
+				} else {
+					ssize = humanize.Bytes(uint64(size))
+				}
+				cmd.Printf("%s\t%s\t %s\n", ent.Mode(), ssize, ent.Name())
+			}
+			return nil
+		},
+	}
+	return cmd
+}