Skip to content

Commit

Permalink
feat(server): ssh cli api and middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
aymanbagabas committed May 2, 2023
1 parent d64549c commit c1cbb40
Show file tree
Hide file tree
Showing 17 changed files with 732 additions and 497 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ require (

require (
github.com/aymanbagabas/go-osc52 v1.2.2
github.com/charmbracelet/keygen v0.3.0
github.com/charmbracelet/log v0.2.1
github.com/charmbracelet/ssh v0.0.0-20221117183211-483d43d97103
github.com/gobwas/glob v0.2.3
Expand All @@ -38,7 +39,6 @@ require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/caarlos0/sshmarshal v0.1.0 // indirect
github.com/charmbracelet/keygen v0.3.0 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
Expand Down
179 changes: 179 additions & 0 deletions server/cmd/branch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package cmd

import (
"fmt"
"strings"

"github.com/charmbracelet/soft-serve/git"
gitm "github.com/gogs/git-module"
"github.com/spf13/cobra"
)

func branchCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "branch",
Short: "Manage repository branches",
}

cmd.AddCommand(
branchListCommand(),
branchDefaultCommand(),
branchDeleteCommand(),
)

return cmd
}

func branchListCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "list REPOSITORY",
Short: "List repository branches",
Args: cobra.ExactArgs(1),
PersistentPreRunE: checkIfReadable,
RunE: func(cmd *cobra.Command, args []string) error {
cfg, _ := fromContext(cmd)
rn := strings.TrimSuffix(args[0], ".git")
rr, err := cfg.Backend.Repository(rn)
if err != nil {
return err
}

r, err := rr.Open()
if err != nil {
return err
}

branches, _ := r.Branches()
for _, b := range branches {
cmd.Println(b)
}

return nil
},
}

return cmd
}

func branchDefaultCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "default REPOSITORY [BRANCH]",
Short: "Set or get the default branch",
Args: cobra.RangeArgs(1, 2),
RunE: func(cmd *cobra.Command, args []string) error {
cfg, _ := fromContext(cmd)
rn := strings.TrimSuffix(args[0], ".git")
switch len(args) {
case 1:
if err := checkIfReadable(cmd, args); err != nil {
return err
}
rr, err := cfg.Backend.Repository(rn)
if err != nil {
return err
}

r, err := rr.Open()
if err != nil {
return err
}

head, err := r.HEAD()
if err != nil {
return err
}

cmd.Println(head.Name().Short())
case 2:
if err := checkIfCollab(cmd, args); err != nil {
return err
}

rr, err := cfg.Backend.Repository(rn)
if err != nil {
return err
}

r, err := rr.Open()
if err != nil {
return err
}

branch := args[1]
branches, _ := r.Branches()
var exists bool
for _, b := range branches {
if branch == b {
exists = true
break
}
}

if !exists {
return git.ErrReferenceNotExist
}

if _, err := r.SymbolicRef("HEAD", gitm.RefsHeads+branch); err != nil {
return err
}
}

return nil
},
}

return cmd
}

func branchDeleteCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "delete REPOSITORY BRANCH",
Aliases: []string{"remove", "rm", "del"},
Short: "Delete a branch",
PersistentPreRunE: checkIfCollab,
RunE: func(cmd *cobra.Command, args []string) error {
cfg, _ := fromContext(cmd)
rn := strings.TrimSuffix(args[0], ".git")
rr, err := cfg.Backend.Repository(rn)
if err != nil {
return err
}

r, err := rr.Open()
if err != nil {
return err
}

branch := args[1]
branches, _ := r.Branches()
var exists bool
for _, b := range branches {
if branch == b {
exists = true
break
}
}

if !exists {
return git.ErrReferenceNotExist
}

head, err := r.HEAD()
if err != nil {
return err
}

if head.Name().Short() == branch {
return fmt.Errorf("cannot delete the default branch")
}

if err := r.DeleteBranch(branch, gitm.DeleteBranchOptions{Force: true}); err != nil {
return err
}

return nil
},
}

return cmd
}
121 changes: 87 additions & 34 deletions server/cmd/cmd.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package cmd

import (
"context"
"fmt"
"strings"

"github.com/charmbracelet/soft-serve/server/backend"
"github.com/charmbracelet/soft-serve/server/config"
"github.com/charmbracelet/ssh"
"github.com/charmbracelet/wish"
"github.com/spf13/cobra"
)

Expand All @@ -13,7 +17,7 @@ type ContextKey string

// String returns the string representation of the ContextKey.
func (c ContextKey) String() string {
return "soft-serve cli context key " + string(c)
return string(c) + "ContextKey"
}

var (
Expand All @@ -30,45 +34,27 @@ var (
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}}{{if .HasParent }}
{{.Parent.Use}} {{end}}{{.Use}}{{if .HasAvailableFlags }} [flags]{{end}}{{end}}{{if .HasAvailableSubCommands}}
{{if .HasParent }}{{.Parent.Use}} {{end}}{{.Use}} [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 {
// 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,
Use: "soft",
Short: "Soft Serve is a self-hostable Git server for the command line.",
SilenceUsage: true,
}
rootCmd.SetUsageTemplate(usageTemplate)
// TODO: use command usage template to include hostname and port
rootCmd.CompletionOptions.DisableDefaultCmd = true
rootCmd.AddCommand(
RepoCommand(),
branchCommand(),
createCommand(),
deleteCommand(),
descriptionCommand(),
listCommand(),
privateCommand(),
renameCommand(),
showCommand(),
tagCommand(),
)

return rootCmd
Expand All @@ -80,3 +66,70 @@ func fromContext(cmd *cobra.Command) (*config.Config, ssh.Session) {
s := ctx.Value(SessionCtxKey).(ssh.Session)
return cfg, s
}

func checkIfReadable(cmd *cobra.Command, args []string) error {
var repo string
if len(args) > 0 {
repo = args[0]
}
cfg, s := fromContext(cmd)
rn := strings.TrimSuffix(repo, ".git")
auth := cfg.Access.AccessLevel(rn, s.PublicKey())
if auth < backend.ReadOnlyAccess {
return ErrUnauthorized
}
return nil
}

func checkIfAdmin(cmd *cobra.Command, args []string) error {
cfg, s := fromContext(cmd)
if !cfg.Backend.IsAdmin(s.PublicKey()) {
return ErrUnauthorized
}
return nil
}

func checkIfCollab(cmd *cobra.Command, args []string) error {
var repo string
if len(args) > 0 {
repo = args[0]
}
cfg, s := fromContext(cmd)
rn := strings.TrimSuffix(repo, ".git")
auth := cfg.Access.AccessLevel(rn, s.PublicKey())
if auth < backend.ReadWriteAccess {
return ErrUnauthorized
}
return nil
}

// Middleware is the Soft Serve middleware that handles SSH commands.
func Middleware(cfg *config.Config) wish.Middleware {
return func(sh ssh.Handler) ssh.Handler {
return func(s ssh.Session) {
func() {
_, _, active := s.Pty()
if active {
return
}
ctx := context.WithValue(s.Context(), ConfigCtxKey, cfg)
ctx = context.WithValue(ctx, SessionCtxKey, s)

rootCmd := rootCommand()
rootCmd.SetArgs(s.Command())
if len(s.Command()) == 0 {
// otherwise it'll default to os.Args, which is not what we want.
rootCmd.SetArgs([]string{"--help"})
}
rootCmd.SetIn(s)
rootCmd.SetOut(s)
rootCmd.CompletionOptions.DisableDefaultCmd = true
rootCmd.SetErr(s.Stderr())
if err := rootCmd.ExecuteContext(ctx); err != nil {
_ = s.Exit(1)
}
}()
sh(s)
}
}
}
26 changes: 26 additions & 0 deletions server/cmd/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package cmd

import "github.com/spf13/cobra"

// createCommand is the command for creating a new repository.
func createCommand() *cobra.Command {
var private bool
var description string
cmd := &cobra.Command{
Use: "create REPOSITORY",
Short: "Create a new repository.",
Args: cobra.ExactArgs(1),
PersistentPreRunE: checkIfAdmin,
RunE: func(cmd *cobra.Command, args []string) error {
cfg, _ := fromContext(cmd)
name := args[0]
if _, err := cfg.Backend.CreateRepository(name, private); err != nil {
return err
}
return nil
},
}
cmd.Flags().BoolVarP(&private, "private", "p", false, "make the repository private")
cmd.Flags().StringVarP(&description, "description", "d", "", "set the repository description")
return cmd
}
Loading

0 comments on commit c1cbb40

Please sign in to comment.