Skip to content

Commit

Permalink
feat: add soft-serve middleware commands
Browse files Browse the repository at this point in the history
* list files
* cat files
* reload config
* git command
  • Loading branch information
aymanbagabas committed Apr 7, 2022
1 parent 062cb70 commit 0aec8c2
Show file tree
Hide file tree
Showing 10 changed files with 408 additions and 174 deletions.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ require (
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
github.com/muesli/mango v0.1.0
github.com/muesli/roff v0.1.0
github.com/spf13/cobra v1.4.0
)

require (
Expand All @@ -45,6 +46,7 @@ require (
github.com/go-git/gcfg v1.5.0 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
Expand All @@ -57,6 +59,7 @@ require (
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/sahilm/fuzzy v0.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xanzy/ssh-agent v0.3.1 // indirect
github.com/yuin/goldmark v1.4.4 // indirect
github.com/yuin/goldmark-emoji v1.0.1 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ github.com/charmbracelet/wish v0.3.1-0.20220405152319-bea68c3da3b1/go.mod h1:+Eg
github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
Expand Down Expand Up @@ -76,6 +77,8 @@ github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
Expand Down Expand Up @@ -135,12 +138,17 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
Expand Down
123 changes: 123 additions & 0 deletions server/cmd/cat.go
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
}
70 changes: 70 additions & 0 deletions server/cmd/cmd.go
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
}
54 changes: 54 additions & 0 deletions server/cmd/git.go
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()
}
81 changes: 81 additions & 0 deletions server/cmd/list.go
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
}
Loading

0 comments on commit 0aec8c2

Please sign in to comment.