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}