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}