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