Detailed changes
@@ -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
@@ -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
@@ -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
+}
@@ -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"
)
@@ -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 (
@@ -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
@@ -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)
+ }
+ }
+}
@@ -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
+}
@@ -0,0 +1,22 @@
+package cmd
+
+import "github.com/spf13/cobra"
+
+func deleteCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "delete REPOSITORY",
+ Aliases: []string{"del", "remove", "rm"},
+ Short: "Delete a 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.DeleteRepository(name); err != nil {
+ return err
+ }
+ return nil
+ },
+ }
+ return cmd
+}
@@ -0,0 +1,40 @@
+package cmd
+
+import (
+ "strings"
+
+ "github.com/spf13/cobra"
+)
+
+func descriptionCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "description REPOSITORY [DESCRIPTION]",
+ Aliases: []string{"desc"},
+ Short: "Set or get the description for a repository.",
+ Args: cobra.MinimumNArgs(1),
+ 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
+ }
+
+ desc := cfg.Backend.Description(rn)
+ cmd.Println(desc)
+ default:
+ if err := checkIfCollab(cmd, args); err != nil {
+ return err
+ }
+ if err := cfg.Backend.SetDescription(rn, strings.Join(args[1:], " ")); err != nil {
+ return err
+ }
+ }
+
+ return nil
+ },
+ }
+
+ return cmd
+}
@@ -0,0 +1,95 @@
+package cmd
+
+import (
+ "fmt"
+ "path/filepath"
+ "strings"
+
+ "github.com/charmbracelet/soft-serve/git"
+ "github.com/charmbracelet/soft-serve/server/backend"
+ "github.com/spf13/cobra"
+)
+
+// listCommand returns a command that list file or directory at path.
+func listCommand() *cobra.Command {
+ listCmd := &cobra.Command{
+ Use: "list PATH",
+ Aliases: []string{"ls"},
+ Short: "List files at repository.",
+ Args: cobra.RangeArgs(0, 1),
+ PersistentPreRunE: checkIfReadable,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ cfg, s := fromContext(cmd)
+ rn := ""
+ path := ""
+ ps := []string{}
+ if len(args) > 0 {
+ path = filepath.Clean(args[0])
+ ps = strings.Split(path, "/")
+ rn = strings.TrimSuffix(ps[0], ".git")
+ auth := cfg.Access.AccessLevel(rn, s.PublicKey())
+ if auth < backend.ReadOnlyAccess {
+ return ErrUnauthorized
+ }
+ }
+ if path == "" || path == "." || path == "/" {
+ repos, err := cfg.Backend.Repositories()
+ if err != nil {
+ return err
+ }
+ for _, r := range repos {
+ if cfg.Access.AccessLevel(r.Name(), s.PublicKey()) >= backend.ReadOnlyAccess {
+ cmd.Println(r.Name())
+ }
+ }
+ return nil
+ }
+ 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 {
+ if bs, err := r.Branches(); err != nil && len(bs) == 0 {
+ return fmt.Errorf("repository is empty")
+ }
+ return err
+ }
+ tree, err := r.TreePath(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 {
+ cmd.Printf("%s\t%d\t %s\n", ent.Mode(), ent.Size(), ent.Name())
+ }
+ return nil
+ },
+ }
+ return listCmd
+}
@@ -1,47 +0,0 @@
-package cmd
-
-import (
- "context"
- "fmt"
-
- "github.com/charmbracelet/soft-serve/server/config"
- "github.com/charmbracelet/ssh"
- "github.com/charmbracelet/wish"
-)
-
-// 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)
-
- use := "ssh"
- port := cfg.Backend.ServerPort()
- if port != "22" {
- use += fmt.Sprintf(" -p%s", port)
- }
- use += fmt.Sprintf(" %s", cfg.Backend.ServerHost())
- cmd := RootCommand()
- cmd.Use = use
- cmd.CompletionOptions.DisableDefaultCmd = true
- cmd.SetIn(s)
- cmd.SetOut(s)
- cmd.SetErr(s.Stderr())
- cmd.SetArgs(s.Command())
- err := cmd.ExecuteContext(ctx)
- if err != nil {
- _, _ = s.Write([]byte(err.Error()))
- _ = s.Exit(1)
- return
- }
- }()
- sh(s)
- }
- }
-}
@@ -0,0 +1,44 @@
+package cmd
+
+import (
+ "strconv"
+ "strings"
+
+ "github.com/spf13/cobra"
+)
+
+func privateCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "private REPOSITORY [true|false]",
+ Short: "Set or get a repository private property.",
+ 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
+ }
+
+ isPrivate := cfg.Backend.IsPrivate(rn)
+ cmd.Println(isPrivate)
+ case 2:
+ isPrivate, err := strconv.ParseBool(args[1])
+ if err != nil {
+ return err
+ }
+ if err := checkIfCollab(cmd, args); err != nil {
+ return err
+ }
+ if err := cfg.Backend.SetPrivate(rn, isPrivate); err != nil {
+ return err
+ }
+ }
+ return nil
+ },
+ }
+
+ return cmd
+}
@@ -0,0 +1,23 @@
+package cmd
+
+import "github.com/spf13/cobra"
+
+func renameCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "rename REPOSITORY NEW_NAME",
+ Short: "Rename an existing repository.",
+ Args: cobra.ExactArgs(2),
+ PersistentPreRunE: checkIfCollab,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ cfg, _ := fromContext(cmd)
+ oldName := args[0]
+ newName := args[1]
+ if err := cfg.Backend.RenameRepository(oldName, newName); err != nil {
+ return err
+ }
+ return nil
+ },
+ }
+
+ return cmd
+}
@@ -1,408 +0,0 @@
-package cmd
-
-import (
- "fmt"
- "path/filepath"
- "strconv"
- "strings"
-
- "github.com/alecthomas/chroma/lexers"
- gansi "github.com/charmbracelet/glamour/ansi"
- "github.com/charmbracelet/lipgloss"
- "github.com/charmbracelet/soft-serve/git"
- "github.com/charmbracelet/soft-serve/server/backend"
- "github.com/charmbracelet/soft-serve/ui/common"
- "github.com/muesli/termenv"
- "github.com/spf13/cobra"
-)
-
-// RepoCommand is the command for managing repositories.
-func RepoCommand() *cobra.Command {
- cmd := &cobra.Command{
- Use: "repo COMMAND",
- Aliases: []string{"repository", "repositories"},
- Short: "Manage repositories.",
- }
- cmd.AddCommand(
- setCommand(),
- createCommand(),
- deleteCommand(),
- listCommand(),
- showCommand(),
- )
- return cmd
-}
-
-func setCommand() *cobra.Command {
- cmd := &cobra.Command{
- Use: "set",
- Short: "Set repository properties.",
- }
- cmd.AddCommand(
- setName(),
- setDescription(),
- setPrivate(),
- setDefaultBranch(),
- )
- return cmd
-}
-
-// createCommand is the command for creating a new repository.
-func createCommand() *cobra.Command {
- var private bool
- var description string
- var projectName string
- cmd := &cobra.Command{
- Use: "create REPOSITORY",
- Short: "Create a new repository.",
- Args: cobra.ExactArgs(1),
- PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
- cfg, s := fromContext(cmd)
- if !cfg.Backend.IsAdmin(s.PublicKey()) {
- return ErrUnauthorized
- }
- return nil
- },
- 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")
- cmd.Flags().StringVarP(&projectName, "project-name", "n", "", "set the project name")
- return cmd
-}
-
-func deleteCommand() *cobra.Command {
- cmd := &cobra.Command{
- Use: "delete REPOSITORY",
- Short: "Delete a 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.DeleteRepository(name); err != nil {
- return err
- }
- return nil
- },
- }
- return cmd
-}
-
-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
-}
-
-func setName() *cobra.Command {
- cmd := &cobra.Command{
- Use: "name REPOSITORY NEW_NAME",
- Short: "Set the name for a repository.",
- Args: cobra.ExactArgs(2),
- PersistentPreRunE: checkIfAdmin,
- RunE: func(cmd *cobra.Command, args []string) error {
- cfg, _ := fromContext(cmd)
- oldName := args[0]
- newName := args[1]
- if err := cfg.Backend.RenameRepository(oldName, newName); err != nil {
- return err
- }
- return nil
- },
- }
- return cmd
-}
-
-func setDescription() *cobra.Command {
- cmd := &cobra.Command{
- Use: "description REPOSITORY DESCRIPTION",
- Short: "Set the description for a repository.",
- Args: cobra.MinimumNArgs(2),
- PersistentPreRunE: checkIfCollab,
- RunE: func(cmd *cobra.Command, args []string) error {
- cfg, _ := fromContext(cmd)
- rn := strings.TrimSuffix(args[0], ".git")
- if err := cfg.Backend.SetDescription(rn, strings.Join(args[1:], " ")); err != nil {
- return err
- }
- return nil
- },
- }
- return cmd
-}
-
-func setPrivate() *cobra.Command {
- cmd := &cobra.Command{
- Use: "private REPOSITORY [true|false]",
- Short: "Set a repository to private.",
- Args: cobra.ExactArgs(2),
- PersistentPreRunE: checkIfCollab,
- RunE: func(cmd *cobra.Command, args []string) error {
- cfg, _ := fromContext(cmd)
- rn := strings.TrimSuffix(args[0], ".git")
- isPrivate, err := strconv.ParseBool(args[1])
- if err != nil {
- return err
- }
- if err := cfg.Backend.SetPrivate(rn, isPrivate); err != nil {
- return err
- }
- return nil
- },
- }
- return cmd
-}
-
-func setDefaultBranch() *cobra.Command {
- cmd := &cobra.Command{
- Use: "default-branch REPOSITORY BRANCH",
- Short: "Set the default branch for a repository.",
- Args: cobra.ExactArgs(2),
- PersistentPreRunE: checkIfAdmin,
- RunE: func(cmd *cobra.Command, args []string) error {
- cfg, _ := fromContext(cmd)
- rn := strings.TrimSuffix(args[0], ".git")
- if err := cfg.Backend.SetDefaultBranch(rn, args[1]); err != nil {
- return err
- }
- return nil
- },
- }
- return cmd
-}
-
-// listCommand returns a command that list file or directory at path.
-func listCommand() *cobra.Command {
- listCmd := &cobra.Command{
- Use: "list PATH",
- Aliases: []string{"ls"},
- Short: "List file or directory at path.",
- Args: cobra.RangeArgs(0, 1),
- PersistentPreRunE: checkIfReadable,
- RunE: func(cmd *cobra.Command, args []string) error {
- cfg, s := fromContext(cmd)
- rn := ""
- path := ""
- ps := []string{}
- if len(args) > 0 {
- path = filepath.Clean(args[0])
- ps = strings.Split(path, "/")
- rn = strings.TrimSuffix(ps[0], ".git")
- auth := cfg.Access.AccessLevel(rn, s.PublicKey())
- if auth < backend.ReadOnlyAccess {
- return ErrUnauthorized
- }
- }
- if path == "" || path == "." || path == "/" {
- repos, err := cfg.Backend.Repositories()
- if err != nil {
- return err
- }
- for _, r := range repos {
- if cfg.Access.AccessLevel(r.Name(), s.PublicKey()) >= backend.ReadOnlyAccess {
- fmt.Fprintln(s, r.Name())
- }
- }
- return nil
- }
- rr, err := cfg.Backend.Repository(rn)
- if err != nil {
- return err
- }
- r, err := rr.Repository()
- if err != nil {
- return err
- }
- head, err := r.HEAD()
- if err != nil {
- if bs, err := r.Branches(); err != nil && len(bs) == 0 {
- return fmt.Errorf("repository is empty")
- }
- return err
- }
- tree, err := r.TreePath(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 listCmd
-}
-
-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"))
-)
-
-// showCommand returns a command that prints the contents of a file.
-func showCommand() *cobra.Command {
- var linenumber bool
- var color bool
-
- showCmd := &cobra.Command{
- Use: "show PATH",
- Aliases: []string{"cat"},
- Short: "Outputs the contents of the file at path.",
- Args: cobra.ExactArgs(1),
- PersistentPreRunE: checkIfReadable,
- RunE: func(cmd *cobra.Command, args []string) error {
- cfg, s := fromContext(cmd)
- ps := strings.Split(args[0], "/")
- rn := strings.TrimSuffix(ps[0], ".git")
- fp := strings.Join(ps[1:], "/")
- auth := cfg.Access.AccessLevel(rn, s.PublicKey())
- if auth < backend.ReadOnlyAccess {
- return ErrUnauthorized
- }
- var repo backend.Repository
- repoExists := false
- repos, err := cfg.Backend.Repositories()
- if err != nil {
- return err
- }
- for _, rp := range repos {
- if rp.Name() == rn {
- repoExists = true
- repo = rp
- break
- }
- }
- if !repoExists {
- return ErrRepoNotFound
- }
- c, _, err := backend.LatestFile(repo, 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
- },
- }
- showCmd.Flags().BoolVarP(&linenumber, "linenumber", "l", false, "Print line numbers")
- showCmd.Flags().BoolVarP(&color, "color", "c", false, "Colorize output")
-
- return showCmd
-}
-
-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.StyleConfig()
- 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
-}
@@ -0,0 +1,128 @@
+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/server/backend"
+ "github.com/charmbracelet/soft-serve/ui/common"
+ "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"))
+)
+
+// showCommand returns a command that prints the contents of a file.
+func showCommand() *cobra.Command {
+ var linenumber bool
+ var color bool
+
+ showCmd := &cobra.Command{
+ Use: "show PATH",
+ Aliases: []string{"cat"},
+ Short: "Read the contents of file at path.",
+ Args: cobra.ExactArgs(1),
+ PersistentPreRunE: checkIfReadable,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ cfg, s := fromContext(cmd)
+ ps := strings.Split(args[0], "/")
+ rn := strings.TrimSuffix(ps[0], ".git")
+ fp := strings.Join(ps[1:], "/")
+ auth := cfg.Access.AccessLevel(rn, s.PublicKey())
+ if auth < backend.ReadOnlyAccess {
+ return ErrUnauthorized
+ }
+ var repo backend.Repository
+ repoExists := false
+ repos, err := cfg.Backend.Repositories()
+ if err != nil {
+ return err
+ }
+ for _, rp := range repos {
+ if rp.Name() == rn {
+ repoExists = true
+ repo = rp
+ break
+ }
+ }
+ if !repoExists {
+ return ErrRepoNotFound
+ }
+ c, _, err := backend.LatestFile(repo, fp)
+ if err != nil {
+ return err
+ }
+ if color {
+ c, err = withFormatting(fp, c)
+ if err != nil {
+ return err
+ }
+ }
+ if linenumber {
+ c = withLineNumber(c, color)
+ }
+ cmd.Println(c)
+ return nil
+ },
+ }
+ showCmd.Flags().BoolVarP(&linenumber, "linenumber", "l", false, "Print line numbers")
+ showCmd.Flags().BoolVarP(&color, "color", "c", false, "Colorize output")
+
+ return showCmd
+}
+
+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.StyleConfig()
+ 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
+}
@@ -0,0 +1,80 @@
+package cmd
+
+import (
+ "strings"
+
+ "github.com/spf13/cobra"
+)
+
+func tagCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "tag",
+ Short: "Manage repository tags",
+ }
+
+ cmd.AddCommand(
+ tagListCommand(),
+ tagDeleteCommand(),
+ )
+
+ return cmd
+}
+
+func tagListCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "list REPOSITORY",
+ Aliases: []string{"ls"},
+ Short: "List repository tags",
+ 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
+ }
+
+ tags, _ := r.Tags()
+ for _, t := range tags {
+ cmd.Println(t)
+ }
+
+ return nil
+ },
+ }
+
+ return cmd
+}
+
+func tagDeleteCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "delete REPOSITORY TAG",
+ Aliases: []string{"remove", "rm", "del"},
+ Short: "Delete a tag",
+ Args: cobra.ExactArgs(2),
+ 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
+ }
+
+ return r.DeleteTag(args[1])
+ },
+ }
+
+ return cmd
+}
@@ -331,7 +331,7 @@ func (f *Files) updateFilesCmd() tea.Msg {
log.Printf("ui: files: ref is nil")
return common.ErrorMsg(errNoRef)
}
- r, err := f.repo.Repository()
+ r, err := f.repo.Open()
if err != nil {
return common.ErrorMsg(err)
}
@@ -388,7 +388,7 @@ func (l *Log) countCommitsCmd() tea.Msg {
logger.Debugf("ui: log: ref is nil")
return common.ErrorMsg(errNoRef)
}
- r, err := l.repo.Repository()
+ r, err := l.repo.Open()
if err != nil {
return common.ErrorMsg(err)
}
@@ -418,7 +418,7 @@ func (l *Log) updateCommitsCmd() tea.Msg {
page := l.nextPage
limit := l.selector.PerPage()
skip := page * limit
- r, err := l.repo.Repository()
+ r, err := l.repo.Open()
if err != nil {
return common.ErrorMsg(err)
}
@@ -445,7 +445,7 @@ func (l *Log) selectCommitCmd(commit *git.Commit) tea.Cmd {
}
func (l *Log) loadDiffCmd() tea.Msg {
- r, err := l.repo.Repository()
+ r, err := l.repo.Open()
if err != nil {
logger.Debugf("ui: error loading diff repository: %v", err)
return common.ErrorMsg(err)
@@ -175,7 +175,7 @@ func (r *Refs) StatusBarInfo() string {
func (r *Refs) updateItemsCmd() tea.Msg {
its := make(RefItems, 0)
- rr, err := r.repo.Repository()
+ rr, err := r.repo.Open()
if err != nil {
return common.ErrorMsg(err)
}
@@ -218,7 +218,7 @@ func switchRefCmd(ref *ggit.Reference) tea.Cmd {
// UpdateRefCmd gets the repository's HEAD reference and sends a RefMsg.
func UpdateRefCmd(repo backend.Repository) tea.Cmd {
return func() tea.Msg {
- r, err := repo.Repository()
+ r, err := repo.Open()
if err != nil {
return common.ErrorMsg(err)
}
@@ -56,7 +56,7 @@ type Item struct {
// New creates a new Item.
func NewItem(repo backend.Repository, cfg *config.Config) (Item, error) {
- r, err := repo.Repository()
+ r, err := repo.Open()
if err != nil {
return Item{}, err
}