-
Notifications
You must be signed in to change notification settings - Fork 141
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add soft-serve middleware commands
* list files * cat files * reload config * git command
- Loading branch information
1 parent
062cb70
commit 0aec8c2
Showing
10 changed files
with
408 additions
and
174 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
package cmd | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/alecthomas/chroma/lexers" | ||
gansi "github.com/charmbracelet/glamour/ansi" | ||
"github.com/charmbracelet/lipgloss" | ||
"github.com/charmbracelet/soft-serve/internal/git" | ||
"github.com/charmbracelet/soft-serve/tui/common" | ||
gitwish "github.com/charmbracelet/wish/git" | ||
"github.com/muesli/termenv" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
var ( | ||
lineDigitStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("239")) | ||
lineBarStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("236")) | ||
dirnameStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#00AAFF")) | ||
filenameStyle = lipgloss.NewStyle() | ||
filemodeStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#777777")) | ||
) | ||
|
||
// CatCommand returns a command that prints the contents of a file. | ||
func CatCommand() *cobra.Command { | ||
var linenumber bool | ||
var color bool | ||
|
||
catCmd := &cobra.Command{ | ||
Use: "cat PATH", | ||
Short: "Outputs the contents of the file at path.", | ||
Args: cobra.ExactArgs(1), | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
ac, s := fromContext(cmd) | ||
ps := strings.Split(args[0], "/") | ||
rn := ps[0] | ||
fp := strings.Join(ps[1:], "/") | ||
auth := ac.AuthRepo(rn, s.PublicKey()) | ||
if auth < gitwish.ReadOnlyAccess { | ||
return ErrUnauthorized | ||
} | ||
var repo *git.Repo | ||
repoExists := false | ||
for _, rp := range ac.Source.AllRepos() { | ||
if rp.Name() == rn { | ||
repoExists = true | ||
repo = rp | ||
break | ||
} | ||
} | ||
if !repoExists { | ||
return ErrRepoNotFound | ||
} | ||
c, _, err := repo.LatestFile(fp) | ||
if err != nil { | ||
return err | ||
} | ||
if color { | ||
c, err = withFormatting(fp, c) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
if linenumber { | ||
c = withLineNumber(c, color) | ||
} | ||
fmt.Fprint(s, c) | ||
return nil | ||
}, | ||
} | ||
catCmd.Flags().BoolVarP(&linenumber, "linenumber", "l", false, "Print line numbers") | ||
catCmd.Flags().BoolVarP(&color, "color", "c", false, "Colorize output") | ||
|
||
return catCmd | ||
} | ||
|
||
func withLineNumber(s string, color bool) string { | ||
lines := strings.Split(s, "\n") | ||
// NB: len() is not a particularly safe way to count string width (because | ||
// it's counting bytes instead of runes) but in this case it's okay | ||
// because we're only dealing with digits, which are one byte each. | ||
mll := len(fmt.Sprintf("%d", len(lines))) | ||
for i, l := range lines { | ||
digit := fmt.Sprintf("%*d", mll, i+1) | ||
bar := "│" | ||
if color { | ||
digit = lineDigitStyle.Render(digit) | ||
bar = lineBarStyle.Render(bar) | ||
} | ||
if i < len(lines)-1 || len(l) != 0 { | ||
// If the final line was a newline we'll get an empty string for | ||
// the final line, so drop the newline altogether. | ||
lines[i] = fmt.Sprintf(" %s %s %s", digit, bar, l) | ||
} | ||
} | ||
return strings.Join(lines, "\n") | ||
} | ||
|
||
func withFormatting(p, c string) (string, error) { | ||
zero := uint(0) | ||
lang := "" | ||
lexer := lexers.Match(p) | ||
if lexer != nil && lexer.Config() != nil { | ||
lang = lexer.Config().Name | ||
} | ||
formatter := &gansi.CodeBlockElement{ | ||
Code: c, | ||
Language: lang, | ||
} | ||
r := strings.Builder{} | ||
styles := common.DefaultStyles() | ||
styles.CodeBlock.Margin = &zero | ||
rctx := gansi.NewRenderContext(gansi.Options{ | ||
Styles: styles, | ||
ColorProfile: termenv.TrueColor, | ||
}) | ||
err := formatter.Render(&r, rctx) | ||
if err != nil { | ||
return "", err | ||
} | ||
return r.String(), nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
package cmd | ||
|
||
import ( | ||
"fmt" | ||
|
||
appCfg "github.com/charmbracelet/soft-serve/internal/config" | ||
"github.com/gliderlabs/ssh" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
var ( | ||
// ErrUnauthorized is returned when the user is not authorized to perform action. | ||
ErrUnauthorized = fmt.Errorf("Unauthorized") | ||
// ErrRepoNotFound is returned when the repo is not found. | ||
ErrRepoNotFound = fmt.Errorf("Repository not found") | ||
// ErrFileNotFound is returned when the file is not found. | ||
ErrFileNotFound = fmt.Errorf("File not found") | ||
|
||
usageTemplate = `Usage:{{if .Runnable}} | ||
{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} | ||
{{.UseLine}} [command]{{end}}{{if gt (len .Aliases) 0}} | ||
Aliases: | ||
{{.NameAndAliases}}{{end}}{{if .HasExample}} | ||
Examples: | ||
{{.Example}}{{end}}{{if .HasAvailableSubCommands}} | ||
Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} | ||
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} | ||
Flags: | ||
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} | ||
Global Flags: | ||
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} | ||
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} | ||
{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} | ||
Use "{{.UseLine}} [command] --help" for more information about a command.{{end}} | ||
` | ||
) | ||
|
||
// RootCommand is the root command for the server. | ||
func RootCommand() *cobra.Command { | ||
rootCmd := &cobra.Command{ | ||
Use: "ssh [-p PORT] HOST", | ||
Long: "Soft Serve is a self-hostable Git server for the command line.", | ||
Args: cobra.MinimumNArgs(1), | ||
DisableFlagsInUseLine: true, | ||
} | ||
rootCmd.SetUsageTemplate(usageTemplate) | ||
rootCmd.CompletionOptions.DisableDefaultCmd = true | ||
rootCmd.AddCommand( | ||
ReloadCommand(), | ||
CatCommand(), | ||
ListCommand(), | ||
GitCommand(), | ||
) | ||
|
||
return rootCmd | ||
} | ||
|
||
func fromContext(cmd *cobra.Command) (*appCfg.Config, ssh.Session) { | ||
ctx := cmd.Context() | ||
ac := ctx.Value("config").(*appCfg.Config) | ||
s := ctx.Value("session").(ssh.Session) | ||
return ac, s | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package cmd | ||
|
||
import ( | ||
"io" | ||
"os/exec" | ||
|
||
"github.com/charmbracelet/soft-serve/internal/git" | ||
gitwish "github.com/charmbracelet/wish/git" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
// GitCommand returns a command that handles Git operations. | ||
func GitCommand() *cobra.Command { | ||
gitCmd := &cobra.Command{ | ||
Use: "git REPO COMMAND", | ||
Short: "Perform Git operations on a repository.", | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
ac, s := fromContext(cmd) | ||
auth := ac.AuthRepo("config", s.PublicKey()) | ||
if auth < gitwish.AdminAccess { | ||
return ErrUnauthorized | ||
} | ||
if len(args) < 1 { | ||
return runGit(nil, s, s, "") | ||
} | ||
var repo *git.Repo | ||
rn := args[0] | ||
repoExists := false | ||
for _, rp := range ac.Source.AllRepos() { | ||
if rp.Name() == rn { | ||
repoExists = true | ||
repo = rp | ||
break | ||
} | ||
} | ||
if !repoExists { | ||
return ErrRepoNotFound | ||
} | ||
return runGit(nil, s, s, repo.Path(), args[1:]...) | ||
}, | ||
} | ||
gitCmd.Flags().SetInterspersed(false) | ||
|
||
return gitCmd | ||
} | ||
|
||
func runGit(in io.Reader, out, err io.Writer, dir string, args ...string) error { | ||
cmd := exec.Command("git", args...) | ||
cmd.Stdin = in | ||
cmd.Stdout = out | ||
cmd.Stderr = err | ||
cmd.Dir = dir | ||
return cmd.Run() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
package cmd | ||
|
||
import ( | ||
"fmt" | ||
"path/filepath" | ||
"strings" | ||
|
||
"github.com/charmbracelet/soft-serve/git" | ||
gitwish "github.com/charmbracelet/wish/git" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
// ListCommand returns a command that list file or directory at path. | ||
func ListCommand() *cobra.Command { | ||
lsCmd := &cobra.Command{ | ||
Use: "ls PATH", | ||
Aliases: []string{"list"}, | ||
Short: "List file or directory at path.", | ||
Args: cobra.RangeArgs(0, 1), | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
ac, s := fromContext(cmd) | ||
rn := "" | ||
path := "" | ||
ps := []string{} | ||
if len(args) > 0 { | ||
path = filepath.Clean(args[0]) | ||
ps = strings.Split(path, "/") | ||
rn = ps[0] | ||
auth := ac.AuthRepo(rn, s.PublicKey()) | ||
if auth < gitwish.ReadOnlyAccess { | ||
return ErrUnauthorized | ||
} | ||
} | ||
if path == "" || path == "." || path == "/" { | ||
for _, r := range ac.Source.AllRepos() { | ||
fmt.Fprintln(s, r.Name()) | ||
} | ||
return nil | ||
} | ||
r, err := ac.Source.GetRepo(rn) | ||
if err != nil { | ||
return err | ||
} | ||
head, err := r.HEAD() | ||
if err != nil { | ||
return err | ||
} | ||
tree, err := r.Tree(head, "") | ||
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 | ||
} | ||
ents, err = tree.Entries() | ||
if err != nil { | ||
return err | ||
} | ||
} else { | ||
ents = append(ents, te) | ||
} | ||
ents.Sort() | ||
for _, ent := range ents { | ||
fmt.Fprintf(s, "%s\t%d\t %s\n", ent.Mode(), ent.Size(), ent.Name()) | ||
} | ||
return nil | ||
}, | ||
} | ||
return lsCmd | ||
} |
Oops, something went wrong.