diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000000000000000000000000000000000..d9c9c2fd60d48c909e761b6f6e0f6bc7600216c9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +trim_trailing_whitespace=true +indent_size=2 +indent_style=space + +[*.go] +indent_size=4 +indent_style=tab diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..6f952299270eed506a1dd30028ab8e0f1be7ef7f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# To prevent CRLF breakages on Windows for fragile files, like testdata. +* -text diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8985110722eb708e717ca51d2f18be33ea8b066e..fb6b091140633fe1e1c541bc242b3941f2873ddd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,4 +25,4 @@ jobs: run: go test -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt ./... -timeout 5m - uses: codecov/codecov-action@v3 with: - file: ./coverage.txt \ No newline at end of file + file: ./coverage.txt diff --git a/go.mod b/go.mod index 7bb2d9ab3c84d859eeadf339d47f252312dbace0..aa047c9382a1a8096b2310a5b51e4854c775f180 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( github.com/muesli/roff v0.1.0 github.com/prometheus/client_golang v1.15.1 github.com/robfig/cron/v3 v3.0.1 + github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97 github.com/spf13/cobra v1.7.0 go.uber.org/automaxprocs v1.5.2 goji.io v2.0.2+incompatible @@ -73,12 +74,11 @@ require ( github.com/prometheus/procfs v0.9.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.2.0 // indirect - github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/sahilm/fuzzy v0.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/yuin/goldmark v1.5.2 // indirect github.com/yuin/goldmark-emoji v1.0.1 // indirect - golang.org/x/mod v0.8.0 // indirect + golang.org/x/mod v0.9.0 // indirect golang.org/x/net v0.10.0 // indirect golang.org/x/sys v0.8.0 // indirect golang.org/x/term v0.8.0 // indirect diff --git a/go.sum b/go.sum index 3d6d4e70bcf85e47b9650c2a1bb73a018b63de66..15bb643da3cc32b12bfd529e8b91eb251769d323 100644 --- a/go.sum +++ b/go.sum @@ -153,8 +153,8 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97 h1:3RPlVWzZ/PDqmVuf/FKHARG5EMid/tl7cv54Sw/QRVY= +github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 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= @@ -185,8 +185,8 @@ goji.io v2.0.2+incompatible/go.mod h1:sbqFwrtqZACxLBTQcdgVjFh54yGVCvwq8+w49MVMMI golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= diff --git a/server/backend/sqlite/user.go b/server/backend/sqlite/user.go index 7306c5cfa4e7d6f57ffd58258960eb15eadb8b55..5e977a6a7af677b27618eb4baa20f6904f578e3e 100644 --- a/server/backend/sqlite/user.go +++ b/server/backend/sqlite/user.go @@ -42,7 +42,8 @@ func (u *User) PublicKeys() []ssh.PublicKey { if err := tx.Select(&keyStrings, `SELECT public_key FROM public_key INNER JOIN user ON user.id = public_key.user_id - WHERE user.username = ?;`, u.username); err != nil { + WHERE user.username = ? + ORDER BY public_key.id asc;`, u.username); err != nil { return err } diff --git a/server/cmd/repo.go b/server/cmd/repo.go index a072f19b88f88ede048c1176429842ee07026733..ae87361b585f1cc87123e32f3e410f9bce14cd4c 100644 --- a/server/cmd/repo.go +++ b/server/cmd/repo.go @@ -1,6 +1,11 @@ package cmd -import "github.com/spf13/cobra" +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" +) func repoCommand() *cobra.Command { cmd := &cobra.Command{ @@ -53,9 +58,12 @@ func repoCommand() *cobra.Command { branches, _ := r.Branches() tags, _ := r.Tags() - cmd.Println("Project Name:", rr.ProjectName()) + + // project name and description are optional, handle trailing + // whitespace to avoid breaking tests. + cmd.Println(strings.TrimSpace(fmt.Sprint("Project Name: ", rr.ProjectName()))) cmd.Println("Repository:", rr.Name()) - cmd.Println("Description:", rr.Description()) + cmd.Println(strings.TrimSpace(fmt.Sprint("Description: ", rr.Description()))) cmd.Println("Private:", rr.IsPrivate()) cmd.Println("Hidden:", rr.IsHidden()) cmd.Println("Mirror:", rr.IsMirror()) diff --git a/server/hooks/hooks.go b/server/hooks/hooks.go index f7606bdd599d66ffcc0bb4cac24684fb1b1a3ca8..c625769f27b31039fe3657969b1e37af6791d61b 100644 --- a/server/hooks/hooks.go +++ b/server/hooks/hooks.go @@ -3,6 +3,7 @@ package hooks import ( "bytes" "context" + "flag" "fmt" "os" "path/filepath" @@ -28,8 +29,13 @@ const ( // - post-update // // This function should be called by the backend when a repository is created. -// TODO: support context -func GenerateHooks(ctx context.Context, cfg *config.Config, repo string) error { +// TODO: support context. +func GenerateHooks(_ context.Context, cfg *config.Config, repo string) error { + // TODO: support git hook tests. + if flag.Lookup("test.v") != nil { + log.WithPrefix("backend.hooks").Warn("refusing to set up hooks when in test") + return nil + } repo = utils.SanitizeRepo(repo) + ".git" hooksPath := filepath.Join(cfg.DataPath, "repos", repo, "hooks") if err := os.MkdirAll(hooksPath, os.ModePerm); err != nil { @@ -136,10 +142,9 @@ done ` ) -var ( - // hooksTmpl is the soft-serve hook that will be run by the git hooks - // inside the hooks directory. - hooksTmpl = template.Must(template.New("hooks").Parse(`#!/usr/bin/env bash +// hooksTmpl is the soft-serve hook that will be run by the git hooks +// inside the hooks directory. +var hooksTmpl = template.Must(template.New("hooks").Parse(`#!/usr/bin/env bash # AUTO GENERATED BY SOFT SERVE, DO NOT MODIFY if [ -z "$SOFT_SERVE_REPO_NAME" ]; then echo "Warning: SOFT_SERVE_REPO_NAME not defined. Skipping hooks." @@ -149,4 +154,3 @@ fi {{ $env }} \{{ end }} {{ .Executable }} hook --config "{{ .Config }}" {{ .Hook }} {{ .Args }} `)) -) diff --git a/server/jobs.go b/server/jobs.go index 9cd23a23f1ad78fc1eed2f32ae8f63e6f5b497bb..239b08d8bf6f1c89876c6f2b447ba8774c4b7729 100644 --- a/server/jobs.go +++ b/server/jobs.go @@ -51,7 +51,6 @@ func (s *Server) mirrorJob() func() { if _, err := cmd.RunInDir(r.Path); err != nil { logger.Error("error running git remote update", "repo", name, "err", err) } - }) } } diff --git a/server/server.go b/server/server.go index 690c9650ba37c47e50780704225c0b05ab625331..1c0470af365ea9a8ba70158b298262dd26073f7a 100644 --- a/server/server.go +++ b/server/server.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "io" "net/http" "github.com/charmbracelet/log" @@ -158,6 +159,9 @@ func (s *Server) Shutdown(ctx context.Context) error { s.Cron.Stop() return nil }) + if closer, ok := s.Backend.(io.Closer); ok { + defer closer.Close() // nolint: errcheck + } return errg.Wait() } @@ -172,5 +176,8 @@ func (s *Server) Close() error { s.Cron.Stop() return nil }) + if closer, ok := s.Backend.(io.Closer); ok { + defer closer.Close() // nolint: errcheck + } return errg.Wait() } diff --git a/server/test/test.go b/server/test/test.go index a9d4d0eca7ac48899385dd23254c66f1249e2855..bfaac42c594791536d233685a87bf28f37dc2ba5 100644 --- a/server/test/test.go +++ b/server/test/test.go @@ -1,11 +1,29 @@ package test -import "net" +import ( + "net" + "sync" +) + +var ( + used = map[int]struct{}{} + lock sync.Mutex +) // RandomPort returns a random port number. // This is mainly used for testing. func RandomPort() int { addr, _ := net.Listen("tcp", ":0") //nolint:gosec _ = addr.Close() - return addr.Addr().(*net.TCPAddr).Port + port := addr.Addr().(*net.TCPAddr).Port + lock.Lock() + + if _, ok := used[port]; ok { + lock.Unlock() + return RandomPort() + } + + used[port] = struct{}{} + lock.Unlock() + return port } diff --git a/server/utils/utils.go b/server/utils/utils.go index f3b01eb30cbf16da473366aeaf0be22e77b016e0..a98cb139c51e853c45a6c9630e277f9bd313403f 100644 --- a/server/utils/utils.go +++ b/server/utils/utils.go @@ -2,7 +2,7 @@ package utils import ( "fmt" - "path/filepath" + "path" "strings" "unicode" ) @@ -10,7 +10,9 @@ import ( // SanitizeRepo returns a sanitized version of the given repository name. func SanitizeRepo(repo string) string { repo = strings.TrimPrefix(repo, "/") - repo = filepath.Clean(repo) + // We're using path instead of filepath here because this is not OS dependent + // looking at you Windows + repo = path.Clean(repo) repo = strings.TrimSuffix(repo, ".git") return repo } diff --git a/server/utils/utils_test.go b/server/utils/utils_test.go new file mode 100644 index 0000000000000000000000000000000000000000..33c1c7cda5af50436c0e0237cfbb287b36f832bc --- /dev/null +++ b/server/utils/utils_test.go @@ -0,0 +1,56 @@ +package utils + +import "testing" + +func TestValidateRepo(t *testing.T) { + t.Run("valid", func(t *testing.T) { + for _, repo := range []string{ + "lower", + "Upper", + "with-dash", + "with/slash", + "withnumb3r5", + "with.dot", + "with_underline", + } { + t.Run(repo, func(t *testing.T) { + if err := ValidateRepo(repo); err != nil { + t.Errorf("expected no error, got %v", err) + } + }) + } + }) + t.Run("invalid", func(t *testing.T) { + for _, repo := range []string{ + "with$", + "with@", + "with!", + } { + t.Run(repo, func(t *testing.T) { + if err := ValidateRepo(repo); err == nil { + t.Error("expected an error, got nil") + } + }) + } + }) +} + +func TestSanitizeRepo(t *testing.T) { + cases := []struct { + in, out string + }{ + {"lower", "lower"}, + {"Upper", "Upper"}, + {"with/slash", "with/slash"}, + {"with.dot", "with.dot"}, + {"/with_forward_slash", "with_forward_slash"}, + {"withgitsuffix.git", "withgitsuffix"}, + } + for _, c := range cases { + t.Run(c.in, func(t *testing.T) { + if got := SanitizeRepo(c.in); got != c.out { + t.Errorf("expected %q, got %q", c.out, got) + } + }) + } +} diff --git a/testscript/script_test.go b/testscript/script_test.go new file mode 100644 index 0000000000000000000000000000000000000000..477edc033823c43b8df3722113f59a493d1a83d0 --- /dev/null +++ b/testscript/script_test.go @@ -0,0 +1,229 @@ +package testscript + +import ( + "bytes" + "context" + "flag" + "fmt" + "net" + "os" + "path/filepath" + "strings" + "sync" + "testing" + "time" + + "github.com/charmbracelet/keygen" + "github.com/charmbracelet/soft-serve/server" + "github.com/charmbracelet/soft-serve/server/config" + "github.com/charmbracelet/soft-serve/server/test" + "github.com/rogpeppe/go-internal/testscript" + "golang.org/x/crypto/ssh" +) + +var update = flag.Bool("update", false, "update script files") + +func TestScript(t *testing.T) { + flag.Parse() + var lock sync.Mutex + + mkkey := func(name string) (string, *keygen.SSHKeyPair) { + path := filepath.Join(t.TempDir(), name) + pair, err := keygen.New(path, keygen.WithKeyType(keygen.Ed25519), keygen.WithWrite()) + if err != nil { + t.Fatal(err) + } + return path, pair + } + + key, admin1 := mkkey("admin1") + _, admin2 := mkkey("admin2") + _, user1 := mkkey("user1") + + testscript.Run(t, testscript.Params{ + Dir: "./testdata/", + UpdateScripts: *update, + Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){ + "soft": cmdSoft(admin1.Signer()), + "git": cmdGit(key), + "mkreadme": cmdMkReadme, + "dos2unix": cmdDos2Unix, + }, + Setup: func(e *testscript.Env) error { + sshPort := test.RandomPort() + e.Setenv("SSH_PORT", fmt.Sprintf("%d", sshPort)) + e.Setenv("ADMIN1_AUTHORIZED_KEY", admin1.AuthorizedKey()) + e.Setenv("ADMIN2_AUTHORIZED_KEY", admin2.AuthorizedKey()) + e.Setenv("USER1_AUTHORIZED_KEY", user1.AuthorizedKey()) + e.Setenv("SSH_KNOWN_HOSTS_FILE", filepath.Join(t.TempDir(), "known_hosts")) + e.Setenv("SSH_KNOWN_CONFIG_FILE", filepath.Join(t.TempDir(), "config")) + data := t.TempDir() + cfg := config.Config{ + Name: "Test Soft Serve", + DataPath: data, + InitialAdminKeys: []string{admin1.AuthorizedKey()}, + SSH: config.SSHConfig{ + ListenAddr: fmt.Sprintf("localhost:%d", sshPort), + PublicURL: fmt.Sprintf("ssh://localhost:%d", sshPort), + KeyPath: filepath.Join(data, "ssh", "soft_serve_host_ed25519"), + ClientKeyPath: filepath.Join(data, "ssh", "soft_serve_client_ed25519"), + }, + Git: config.GitConfig{ + ListenAddr: fmt.Sprintf("localhost:%d", test.RandomPort()), + IdleTimeout: 3, + MaxConnections: 32, + }, + HTTP: config.HTTPConfig{ + ListenAddr: fmt.Sprintf("localhost:%d", test.RandomPort()), + PublicURL: fmt.Sprintf("http://localhost:%d", test.RandomPort()), + }, + Stats: config.StatsConfig{ + ListenAddr: fmt.Sprintf("localhost:%d", test.RandomPort()), + }, + Log: config.LogConfig{ + Format: "text", + TimeFormat: time.DateTime, + }, + } + ctx := config.WithContext(context.Background(), &cfg) + + // prevent race condition in lipgloss... + // this will probably be autofixed when we start using the colors + // from the ssh session instead of the server. + // XXX: take another look at this soon + lock.Lock() + srv, err := server.NewServer(ctx) + if err != nil { + return err + } + lock.Unlock() + + go func() { + if err := srv.Start(); err != nil { + e.T().Fatal(err) + } + }() + + e.Defer(func() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + e.T().Fatal(err) + } + }) + + // wait until the server is up + for { + conn, _ := net.DialTimeout( + "tcp", + net.JoinHostPort("localhost", fmt.Sprintf("%d", sshPort)), + time.Second, + ) + if conn != nil { + conn.Close() + break + } + } + + return nil + }, + }) +} + +func cmdSoft(key ssh.Signer) func(ts *testscript.TestScript, neg bool, args []string) { + return func(ts *testscript.TestScript, neg bool, args []string) { + cli, err := ssh.Dial( + "tcp", + net.JoinHostPort("localhost", ts.Getenv("SSH_PORT")), + &ssh.ClientConfig{ + User: "admin", + Auth: []ssh.AuthMethod{ssh.PublicKeys(key)}, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + }, + ) + ts.Check(err) + defer cli.Close() + + sess, err := cli.NewSession() + ts.Check(err) + defer sess.Close() + + sess.Stdout = ts.Stdout() + sess.Stderr = ts.Stderr() + + check(ts, sess.Run(strings.Join(args, " ")), neg) + } +} + +// P.S. Windows sucks! +func cmdDos2Unix(ts *testscript.TestScript, neg bool, args []string) { + if neg { + ts.Fatalf("unsupported: ! dos2unix") + } + if len(args) < 1 { + ts.Fatalf("usage: dos2unix paths...") + } + for _, arg := range args { + filename := ts.MkAbs(arg) + data, err := os.ReadFile(filename) + if err != nil { + ts.Fatalf("%s: %v", filename, err) + } + + // Replace all '\r\n' with '\n'. + data = bytes.ReplaceAll(data, []byte{'\r', '\n'}, []byte{'\n'}) + + if err := os.WriteFile(filename, data, 0o644); err != nil { + ts.Fatalf("%s: %v", filename, err) + } + } +} + +var sshConfig = ` +Host * + UserKnownHostsFile %q + StrictHostKeyChecking no + IdentityAgent none + IdentitiesOnly yes + ServerAliveInterval 60 +` + +func cmdGit(key string) func(ts *testscript.TestScript, neg bool, args []string) { + return func(ts *testscript.TestScript, neg bool, args []string) { + ts.Check(os.WriteFile( + ts.Getenv("SSH_KNOWN_CONFIG_FILE"), + []byte(fmt.Sprintf(sshConfig, ts.Getenv("SSH_KNOWN_HOSTS_FILE"))), + 0o600, + )) + sshArgs := []string{ + "-F", filepath.ToSlash(ts.Getenv("SSH_KNOWN_CONFIG_FILE")), + "-i", filepath.ToSlash(key), + } + ts.Setenv( + "GIT_SSH_COMMAND", + strings.Join(append([]string{"ssh"}, sshArgs...), " "), + ) + args = append([]string{ + "-c", "user.email=john@example.com", + "-c", "user.name=John Doe", + }, args...) + check(ts, ts.Exec("git", args...), neg) + } +} + +func cmdMkReadme(ts *testscript.TestScript, neg bool, args []string) { + if len(args) != 1 { + ts.Fatalf("usage: mkreadme path") + } + content := []byte("# example\ntest project") + check(ts, os.WriteFile(ts.MkAbs(args[0]), content, 0o644), neg) +} + +func check(ts *testscript.TestScript, err error, neg bool) { + if neg && err == nil { + ts.Fatalf("expected error, got nil") + } + if !neg { + ts.Check(err) + } +} diff --git a/testscript/testdata/mirror.txtar b/testscript/testdata/mirror.txtar new file mode 100644 index 0000000000000000000000000000000000000000..7dcdb6c59b40aa11edc15f6d7f90d1414f4aabc5 --- /dev/null +++ b/testscript/testdata/mirror.txtar @@ -0,0 +1,96 @@ +# vi: set ft=conf + +# convert crlf to lf on windows +[windows] dos2unix info1.txt info2.txt tree.txt + +# import a repo +soft repo import --mirror charmbracelet/catwalk https://github.com/charmbracelet/catwalk.git + +# check repo info +soft repo info charmbracelet/catwalk +cmp stdout info1.txt + +# check repo list +soft repo list +stdout charmbracelet/catwalk + +# is-mirror? +soft repo is-mirror charmbracelet/catwalk +stdout true + +# set project name +soft repo project-name charmbracelet/catwalk catwalk +soft repo list +stdout catwalk + + +# check description +soft repo description charmbracelet/catwalk +! stdout . + +# set description +soft repo description charmbracelet/catwalk "testing repo" +soft repo description charmbracelet/catwalk +stdout 'testing repo' + +# rename +soft repo rename charmbracelet/catwalk charmbracelet/test +soft repo list +stdout charmbracelet/test # TODO: shouldn't this still show the project-name? + +# check its not private +soft repo private charmbracelet/test +stdout false + +# make it private +soft repo private charmbracelet/test true +soft repo private charmbracelet/test +stdout true + +# check its not hidden +soft repo hidden charmbracelet/test +stdout false + +# make it hidden +soft repo hidden charmbracelet/test true +soft repo hidden charmbracelet/test +stdout true + +# print tree +soft repo tree charmbracelet/test +cmp stdout tree.txt + +# check repo info again +soft repo info charmbracelet/test +cmp stdout info2.txt + +# get a file +soft repo blob charmbracelet/test LICENSE +stdout '.*Creative Commons.*' + + +-- info1.txt -- +Project Name: +Repository: charmbracelet/catwalk +Description: +Private: false +Hidden: false +Mirror: true +Default Branch: main +Branches: + - main +-- info2.txt -- +Project Name: catwalk +Repository: charmbracelet/test +Description: testing repo +Private: true +Hidden: true +Mirror: true +Default Branch: main +Branches: + - main +-- tree.txt -- +drwxrwxrwx - 30k +drwxrwxrwx - 50k +-rw-r--r-- 19 kB LICENSE +-rw-r--r-- 1.1 kB README.md diff --git a/testscript/testdata/repo-collab.txtar b/testscript/testdata/repo-collab.txtar new file mode 100644 index 0000000000000000000000000000000000000000..8f7d501b1922831d34cf3cbf72b44cb47507262c --- /dev/null +++ b/testscript/testdata/repo-collab.txtar @@ -0,0 +1,18 @@ +# vi: set ft=conf +# setup +soft repo import test https://github.com/charmbracelet/catwalk.git +soft user create foo --key "$USER1_AUTHORIZED_KEY" + +# list collabs +soft repo collab list test +! stdout . + +# add collab +soft repo collab add test foo +soft repo collab list test +stdout 'foo' + +# remove collab +soft repo collab remove test foo +soft repo collab list test +! stdout . diff --git a/testscript/testdata/repo-create.txtar b/testscript/testdata/repo-create.txtar new file mode 100644 index 0000000000000000000000000000000000000000..108790cada62f112e21f385da48fe9d6d816465f --- /dev/null +++ b/testscript/testdata/repo-create.txtar @@ -0,0 +1,78 @@ +# vi: set ft=conf + +# convert crlf to lf on windows +[windows] dos2unix tree.txt readme.md branch_list.1.txt + +# create a repo +soft repo create repo1 -d 'description' -H -p -n 'repo11' +soft repo hidden repo1 +stdout true +soft repo private repo1 +stdout true +soft repo description repo1 +stdout 'description' + +# clone repo +git clone ssh://localhost:$SSH_PORT/repo1 repo1 + +# create some files, commits, tags... +mkreadme ./repo1/README.md +git -C repo1 add -A +git -C repo1 commit -m 'first' +git -C repo1 tag v0.1.0 +git -C repo1 push origin HEAD +git -C repo1 push origin HEAD --tags + +# list tags +soft repo tag list repo1 +stdout 'v0.1.0' + +# delete tag +soft repo tag delete repo1 v0.1.0 +soft repo tag list repo1 +! stdout . + +# print tree +soft repo tree repo1 +cmp stdout tree.txt + +# cat blob +soft repo blob repo1 README.md +cmp stdout readme.md + +# cat blob that doesn't exist +! soft repo blob repo1 README.txt +! stdout . +stderr '.*revision does not exist.*' + +# check main branch +soft repo branch default repo1 +stdout master + +# create a new branch +git -C repo1 checkout -b branch1 +git -C repo1 push origin branch1 +soft repo branch list repo1 +cmp stdout branch_list.1.txt + +# change default branch +soft repo branch default repo1 branch1 +soft repo branch default repo1 +stdout branch1 + +# cannot delete main branch +! soft repo branch delete repo1 branch1 + +# delete other branch +soft repo branch delete repo1 master +soft repo branch list repo1 +stdout branch1 + +-- tree.txt -- +-rw-r--r-- 22 B README.md +-- readme.md -- +# example +test project +-- branch_list.1.txt -- +branch1 +master diff --git a/testscript/testdata/repo-import.txt b/testscript/testdata/repo-import.txt new file mode 100644 index 0000000000000000000000000000000000000000..4d73a5a43ef7d34cc42f0a85fdba2a44a313753e --- /dev/null +++ b/testscript/testdata/repo-import.txt @@ -0,0 +1,30 @@ +# vi: set ft=conf + +# convert crlf to lf on windows +[windows] dos2unix repo3.txt + +# import private +soft repo import --private repo1 https://github.com/charmbracelet/catwalk.git +soft repo private repo1 +stdout 'true' + +# import hidden +soft repo import --hidden repo2 https://github.com/charmbracelet/catwalk.git +soft repo hidden repo2 +stdout 'true' + +# import with name and description +soft repo import --name 'repo33' --description 'descriptive' repo3 https://github.com/charmbracelet/catwalk.git +soft repo info repo3 +cmp stdout repo3.txt + +-- repo3.txt -- +Project Name: repo33 +Repository: repo3 +Description: descriptive +Private: false +Hidden: false +Mirror: false +Default Branch: main +Branches: + - main diff --git a/testscript/testdata/set-username.txtar b/testscript/testdata/set-username.txtar new file mode 100644 index 0000000000000000000000000000000000000000..03c6e9ce265d302976ac9937db312dd8efca0952 --- /dev/null +++ b/testscript/testdata/set-username.txtar @@ -0,0 +1,24 @@ +# vi: set ft=conf + +# convert crlf to lf on windows +[windows] dos2unix info1.txt info2.txt + +# get original username +soft info +cmpenv stdout info1.txt + +# set another username +soft set-username test +soft info +cmpenv stdout info2.txt + +-- info1.txt -- +Username: admin +Admin: true +Public keys: + $ADMIN1_AUTHORIZED_KEY +-- info2.txt -- +Username: test +Admin: true +Public keys: + $ADMIN1_AUTHORIZED_KEY diff --git a/testscript/testdata/settings.txtar b/testscript/testdata/settings.txtar new file mode 100644 index 0000000000000000000000000000000000000000..606c576c097ae2ccb4656450c368bae077b48900 --- /dev/null +++ b/testscript/testdata/settings.txtar @@ -0,0 +1,31 @@ +# vi: set ft=conf +# check default allow-keyless +soft settings allow-keyless true +soft settings allow-keyless +stdout 'true.*' + +# change allow-keyless and check +soft settings allow-keyless false +soft settings allow-keyless +stdout 'false.*' + +# check default anon-access +soft settings anon-access +stdout 'read-only.*' + +# chaneg anon-access to all available options, and check them +soft settings anon-access no-access +soft settings anon-access +stdout 'no-access.*' + +soft settings anon-access read-only +soft settings anon-access +stdout 'read-only.*' + +soft settings anon-access read-write +soft settings anon-access +stdout 'read-write.*' + +soft settings anon-access admin-access +soft settings anon-access +stdout 'admin-access.*' diff --git a/testscript/testdata/user_management.txtar b/testscript/testdata/user_management.txtar new file mode 100644 index 0000000000000000000000000000000000000000..8eade22e52d8af090cd9501ae8d894901b140b6b --- /dev/null +++ b/testscript/testdata/user_management.txtar @@ -0,0 +1,105 @@ +# vi: set ft=conf + +# convert crlf to lf on windows +[windows] dos2unix info.txt admin_key_list1.txt admin_key_list2.txt list1.txt list2.txt foo_info1.txt foo_info2.txt foo_info3.txt foo_info4.txt foo_info5.txt + +# add key to admin +soft user add-pubkey admin "$ADMIN2_AUTHORIZED_KEY" +soft user info admin +soft info +cmpenv stdout info.txt + + +# list admin pubkeys +soft pubkey list +cmpenv stdout admin_key_list1.txt + +# remove key +soft pubkey remove $ADMIN2_AUTHORIZED_KEY +soft pubkey list +cmpenv stdout admin_key_list2.txt + +# add key back key +soft pubkey add $ADMIN2_AUTHORIZED_KEY +soft pubkey list +cmpenv stdout admin_key_list1.txt + +# list users +soft user list +cmpenv stdout list1.txt + +# create a new user +soft user create foo --key "$USER1_AUTHORIZED_KEY" +soft user list +cmpenv stdout list2.txt + +# get new user info +soft user info foo +cmpenv stdout foo_info1.txt + +# make user admin +soft user set-admin foo true +soft user info foo +cmpenv stdout foo_info2.txt + +# remove admin +soft user set-admin foo false +soft user info foo +cmpenv stdout foo_info3.txt + +# remove key from user +soft user remove-pubkey foo "$USER1_AUTHORIZED_KEY" +soft user info foo +cmpenv stdout foo_info4.txt + +# rename user +soft user set-username foo foo2 +soft user info foo2 +cmpenv stdout foo_info5.txt + +# remove user +soft user delete foo2 +! stdout . +soft user list +cmpenv stdout list1.txt + + +-- info.txt -- +Username: admin +Admin: true +Public keys: + $ADMIN1_AUTHORIZED_KEY + $ADMIN2_AUTHORIZED_KEY +-- list1.txt -- +admin +-- list2.txt -- +admin +foo +-- foo_info1.txt -- +Username: foo +Admin: false +Public keys: + $USER1_AUTHORIZED_KEY +-- foo_info2.txt -- +Username: foo +Admin: true +Public keys: + $USER1_AUTHORIZED_KEY +-- foo_info3.txt -- +Username: foo +Admin: false +Public keys: + $USER1_AUTHORIZED_KEY +-- foo_info4.txt -- +Username: foo +Admin: false +Public keys: +-- foo_info5.txt -- +Username: foo2 +Admin: false +Public keys: +-- admin_key_list1.txt -- +$ADMIN1_AUTHORIZED_KEY +$ADMIN2_AUTHORIZED_KEY +-- admin_key_list2.txt -- +$ADMIN1_AUTHORIZED_KEY