blob.go

  1package cmd
  2
  3import (
  4	"fmt"
  5	"strings"
  6
  7	"github.com/alecthomas/chroma/lexers"
  8	gansi "github.com/charmbracelet/glamour/ansi"
  9	"github.com/charmbracelet/lipgloss"
 10	"github.com/charmbracelet/soft-serve/git"
 11	"github.com/charmbracelet/soft-serve/server/uiutils"
 12	"github.com/muesli/termenv"
 13	"github.com/spf13/cobra"
 14)
 15
 16var (
 17	lineDigitStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("239"))
 18	lineBarStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color("236"))
 19	dirnameStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color("#00AAFF"))
 20	filenameStyle  = lipgloss.NewStyle()
 21	filemodeStyle  = lipgloss.NewStyle().Foreground(lipgloss.Color("#777777"))
 22)
 23
 24// blobCommand returns a command that prints the contents of a file.
 25func blobCommand() *cobra.Command {
 26	var linenumber bool
 27	var color bool
 28	var raw bool
 29
 30	cmd := &cobra.Command{
 31		Use:               "blob REPOSITORY [REFERENCE] [PATH]",
 32		Aliases:           []string{"cat", "show"},
 33		Short:             "Print out the contents of file at path",
 34		Args:              cobra.RangeArgs(1, 3),
 35		PersistentPreRunE: checkIfReadable,
 36		RunE: func(cmd *cobra.Command, args []string) error {
 37			be, _ := fromContext(cmd)
 38			rn := args[0]
 39			ref := ""
 40			fp := ""
 41			switch len(args) {
 42			case 2:
 43				fp = args[1]
 44			case 3:
 45				ref = args[1]
 46				fp = args[2]
 47			}
 48
 49			ctx := cmd.Context()
 50			repo, err := be.Repository(ctx, rn)
 51			if err != nil {
 52				return err
 53			}
 54
 55			r, err := repo.Open()
 56			if err != nil {
 57				return err
 58			}
 59
 60			if ref == "" {
 61				head, err := r.HEAD()
 62				if err != nil {
 63					return err
 64				}
 65				ref = head.Hash.String()
 66			}
 67
 68			tree, err := r.LsTree(ref)
 69			if err != nil {
 70				return err
 71			}
 72
 73			te, err := tree.TreeEntry(fp)
 74			if err != nil {
 75				return err
 76			}
 77
 78			if te.Type() != "blob" {
 79				return git.ErrFileNotFound
 80			}
 81
 82			bts, err := te.Contents()
 83			if err != nil {
 84				return err
 85			}
 86
 87			c := string(bts)
 88			isBin, _ := te.File().IsBinary()
 89			if isBin {
 90				if raw {
 91					cmd.Println(c)
 92				} else {
 93					return fmt.Errorf("binary file: use --raw to print")
 94				}
 95			} else {
 96				if color {
 97					c, err = withFormatting(fp, c)
 98					if err != nil {
 99						return err
100					}
101				}
102
103				if linenumber {
104					c = withLineNumber(c, color)
105				}
106
107				cmd.Println(c)
108			}
109			return nil
110		},
111	}
112
113	cmd.Flags().BoolVarP(&raw, "raw", "r", false, "Print raw contents")
114	cmd.Flags().BoolVarP(&linenumber, "linenumber", "l", false, "Print line numbers")
115	cmd.Flags().BoolVarP(&color, "color", "c", false, "Colorize output")
116
117	return cmd
118}
119
120func withLineNumber(s string, color bool) string {
121	lines := strings.Split(s, "\n")
122	// NB: len() is not a particularly safe way to count string width (because
123	// it's counting bytes instead of runes) but in this case it's okay
124	// because we're only dealing with digits, which are one byte each.
125	mll := len(fmt.Sprintf("%d", len(lines)))
126	for i, l := range lines {
127		digit := fmt.Sprintf("%*d", mll, i+1)
128		bar := "│"
129		if color {
130			digit = lineDigitStyle.Render(digit)
131			bar = lineBarStyle.Render(bar)
132		}
133		if i < len(lines)-1 || len(l) != 0 {
134			// If the final line was a newline we'll get an empty string for
135			// the final line, so drop the newline altogether.
136			lines[i] = fmt.Sprintf(" %s %s %s", digit, bar, l)
137		}
138	}
139	return strings.Join(lines, "\n")
140}
141
142func withFormatting(p, c string) (string, error) {
143	zero := uint(0)
144	lang := ""
145	lexer := lexers.Match(p)
146	if lexer != nil && lexer.Config() != nil {
147		lang = lexer.Config().Name
148	}
149	formatter := &gansi.CodeBlockElement{
150		Code:     c,
151		Language: lang,
152	}
153	r := strings.Builder{}
154	styles := uiutils.StyleConfig()
155	styles.CodeBlock.Margin = &zero
156	rctx := gansi.NewRenderContext(gansi.Options{
157		Styles:       styles,
158		ColorProfile: termenv.TrueColor,
159	})
160	err := formatter.Render(&r, rctx)
161	if err != nil {
162		return "", err
163	}
164	return r.String(), nil
165}