diff --git a/cmd/soft/migrate_config.go b/cmd/soft/migrate_config.go index 4d85c0bd8..583153e5d 100644 --- a/cmd/soft/migrate_config.go +++ b/cmd/soft/migrate_config.go @@ -177,7 +177,7 @@ var migrateConfig = &cobra.Command{ return fmt.Errorf("failed to copy repo: %w", err) } - if _, err := sb.CreateRepository(ctx, dir.Name(), proto.RepositoryOptions{}); err != nil { + if _, err := sb.CreateRepository(ctx, dir.Name(), nil, proto.RepositoryOptions{}); err != nil { fmt.Fprintf(os.Stderr, "failed to create repository: %s\n", err) } } @@ -239,7 +239,7 @@ var migrateConfig = &cobra.Command{ } // Create `.soft-serve` repository and add readme - if _, err := sb.CreateRepository(ctx, ".soft-serve", proto.RepositoryOptions{ + if _, err := sb.CreateRepository(ctx, ".soft-serve", nil, proto.RepositoryOptions{ ProjectName: "Home", Description: "Soft Serve home repository", Hidden: true, diff --git a/server/backend/lfs.go b/server/backend/lfs.go index eac557ecf..5b0d5a406 100644 --- a/server/backend/lfs.go +++ b/server/backend/lfs.go @@ -6,6 +6,7 @@ import ( "io" "path" "path/filepath" + "strconv" "github.com/charmbracelet/soft-serve/server/config" "github.com/charmbracelet/soft-serve/server/db" @@ -18,7 +19,8 @@ import ( // StoreRepoMissingLFSObjects stores missing LFS objects for a repository. func StoreRepoMissingLFSObjects(ctx context.Context, repo proto.Repository, dbx *db.DB, store store.Store, lfsClient lfs.Client) error { cfg := config.FromContext(ctx) - lfsRoot := filepath.Join(cfg.DataPath, "lfs") + repoID := strconv.FormatInt(repo.ID(), 10) + lfsRoot := filepath.Join(cfg.DataPath, "lfs", repoID) // TODO: support S3 storage strg := storage.NewLocalStorage(lfsRoot) diff --git a/server/backend/repo.go b/server/backend/repo.go index 840385112..b31484ba6 100644 --- a/server/backend/repo.go +++ b/server/backend/repo.go @@ -7,8 +7,10 @@ import ( "fmt" "io/fs" "os" + "os/exec" "path" "path/filepath" + "strconv" "time" "github.com/charmbracelet/soft-serve/git" @@ -28,7 +30,7 @@ func (d *Backend) reposPath() string { // CreateRepository creates a new repository. // // It implements backend.Backend. -func (d *Backend) CreateRepository(ctx context.Context, name string, opts proto.RepositoryOptions) (proto.Repository, error) { +func (d *Backend) CreateRepository(ctx context.Context, name string, user proto.User, opts proto.RepositoryOptions) (proto.Repository, error) { name = utils.SanitizeRepo(name) if err := utils.ValidateRepo(name); err != nil { return nil, err @@ -37,11 +39,17 @@ func (d *Backend) CreateRepository(ctx context.Context, name string, opts proto. repo := name + ".git" rp := filepath.Join(d.reposPath(), repo) + var userID int64 + if user != nil { + userID = user.ID() + } + if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error { if err := d.store.CreateRepo( ctx, tx, name, + userID, opts.ProjectName, opts.Description, opts.Private, @@ -72,14 +80,19 @@ func (d *Backend) CreateRepository(ctx context.Context, name string, opts proto. return hooks.GenerateHooks(ctx, d.cfg, repo) }); err != nil { d.logger.Debug("failed to create repository in database", "err", err) - return nil, db.WrapError(err) + err = db.WrapError(err) + if errors.Is(err, db.ErrDuplicateKey) { + return nil, proto.ErrRepoExist + } + + return nil, err } return d.Repository(ctx, name) } // ImportRepository imports a repository from remote. -func (d *Backend) ImportRepository(ctx context.Context, name string, remote string, opts proto.RepositoryOptions) (proto.Repository, error) { +func (d *Backend) ImportRepository(ctx context.Context, name string, user proto.User, remote string, opts proto.RepositoryOptions) (proto.Repository, error) { name = utils.SanitizeRepo(name) if err := utils.ValidateRepo(name); err != nil { return nil, err @@ -92,37 +105,42 @@ func (d *Backend) ImportRepository(ctx context.Context, name string, remote stri return nil, proto.ErrRepoExist } - copts := git.CloneOptions{ - Bare: true, - Mirror: opts.Mirror, - Quiet: true, - CommandOptions: git.CommandOptions{ - Timeout: -1, - Context: ctx, - Envs: []string{ - fmt.Sprintf(`GIT_SSH_COMMAND=ssh -o UserKnownHostsFile="%s" -o StrictHostKeyChecking=no -i "%s"`, - filepath.Join(d.cfg.DataPath, "ssh", "known_hosts"), - d.cfg.SSH.ClientKeyPath, - ), - }, - }, + if err := os.MkdirAll(rp, fs.ModePerm); err != nil { + return nil, err } - if err := git.Clone(remote, rp, copts); err != nil { + cmd := exec.CommandContext(ctx, "git", "clone", "--bare", "--mirror", remote, ".") + cmd.Env = append(cmd.Env, + fmt.Sprintf(`GIT_SSH_COMMAND=ssh -o UserKnownHostsFile="%s" -o StrictHostKeyChecking=no -i "%s"`, + filepath.Join(d.cfg.DataPath, "ssh", "known_hosts"), + d.cfg.SSH.ClientKeyPath, + ), + ) + cmd.Dir = rp + if err := cmd.Run(); err != nil { d.logger.Error("failed to clone repository", "err", err, "mirror", opts.Mirror, "remote", remote, "path", rp) // Cleanup the mess! if rerr := os.RemoveAll(rp); rerr != nil { err = errors.Join(err, rerr) } + return nil, err } - r, err := d.CreateRepository(ctx, name, opts) + r, err := d.CreateRepository(ctx, name, user, opts) if err != nil { d.logger.Error("failed to create repository", "err", err, "name", name) return nil, err } + defer func() { + if err != nil { + if rerr := d.DeleteRepository(ctx, name, opts.LFS); rerr != nil { + d.logger.Error("failed to delete repository", "err", rerr, "name", name) + } + } + }() + rr, err := r.Open() if err != nil { d.logger.Error("failed to open repository", "err", err, "path", rp) @@ -135,20 +153,28 @@ func (d *Backend) ImportRepository(ctx context.Context, name string, remote stri return nil, err } - rcfg.Section("lfs").SetOption("url", remote) + endpoint := remote + if opts.LFSEndpoint != "" { + endpoint = opts.LFSEndpoint + } + + rcfg.Section("lfs").SetOption("url", endpoint) if err := rr.SetConfig(rcfg); err != nil { d.logger.Error("failed to set repository config", "err", err, "path", rp) return nil, err } - endpoint, err := lfs.NewEndpoint(remote) + ep, err := lfs.NewEndpoint(endpoint) if err != nil { d.logger.Error("failed to create lfs endpoint", "err", err, "path", rp) return nil, err } - client := lfs.NewClient(endpoint) + client := lfs.NewClient(ep) + if client == nil { + return nil, fmt.Errorf("failed to create lfs client: unsupported endpoint %s", endpoint) + } if err := StoreRepoMissingLFSObjects(ctx, r, d.db, d.store, client); err != nil { d.logger.Error("failed to store missing lfs objects", "err", err, "path", rp) @@ -171,7 +197,13 @@ func (d *Backend) DeleteRepository(ctx context.Context, name string, deleteLFS b defer d.cache.Delete(name) if deleteLFS { - strg := storage.NewLocalStorage(filepath.Join(d.cfg.DataPath, "lfs")) + repom, err := d.store.GetRepoByName(ctx, tx, name) + if err != nil { + return err + } + + repoID := strconv.FormatInt(repom.ID, 10) + strg := storage.NewLocalStorage(filepath.Join(d.cfg.DataPath, "lfs", repoID)) objs, err := d.store.GetLFSObjectsByName(ctx, tx, name) if err != nil { return err @@ -198,6 +230,29 @@ func (d *Backend) DeleteRepository(ctx context.Context, name string, deleteLFS b }) } +// DeleteUserRepositories deletes all user repositories. +func (d *Backend) DeleteUserRepositories(ctx context.Context, username string, deleteLFS bool) error { + return d.db.TransactionContext(ctx, func(tx *db.Tx) error { + user, err := d.store.FindUserByUsername(ctx, tx, username) + if err != nil { + return err + } + + repos, err := d.store.GetUserRepos(ctx, tx, user.ID) + if err != nil { + return err + } + + for _, repo := range repos { + if err := d.DeleteRepository(ctx, repo.Name, deleteLFS); err != nil { + return err + } + } + + return nil + }) +} + // RenameRepository renames a repository. // // It implements backend.Backend. @@ -501,6 +556,17 @@ func (r *repo) ID() int64 { return r.repo.ID } +// UserID returns the repository's owner's user ID. +// If the repository is not owned by anyone, it returns 0. +// +// It implements proto.Repository. +func (r *repo) UserID() int64 { + if r.repo.UserID.Valid { + return r.repo.UserID.Int64 + } + return 0 +} + // Description returns the repository's description. // // It implements backend.Repository. diff --git a/server/backend/user.go b/server/backend/user.go index 8f1f4bb73..07db904d5 100644 --- a/server/backend/user.go +++ b/server/backend/user.go @@ -62,6 +62,13 @@ func (d *Backend) AccessLevelForUser(ctx context.Context, repo string, user prot } if r != nil { + if user != nil { + // If the user is the owner, they have admin access. + if r.UserID() == user.ID() { + return access.AdminAccess + } + } + // If the user is a collaborator, they have read/write access. isCollab, _ := d.IsCollaborator(ctx, repo, username) if isCollab { @@ -128,6 +135,34 @@ func (d *Backend) User(ctx context.Context, username string) (proto.User, error) }, nil } +// UserByID finds a user by ID. +func (d *Backend) UserByID(ctx context.Context, id int64) (proto.User, error) { + var m models.User + var pks []ssh.PublicKey + if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error { + var err error + m, err = d.store.GetUserByID(ctx, tx, id) + if err != nil { + return err + } + + pks, err = d.store.ListPublicKeysByUserID(ctx, tx, m.ID) + return err + }); err != nil { + err = db.WrapError(err) + if errors.Is(err, db.ErrRecordNotFound) { + return nil, proto.ErrUserNotFound + } + d.logger.Error("error finding user", "id", id, "error", err) + return nil, err + } + + return &user{ + user: m, + publicKeys: pks, + }, nil +} + // UserByPublicKey finds a user by public key. // // It implements backend.Backend. @@ -263,11 +298,13 @@ func (d *Backend) DeleteUser(ctx context.Context, username string) error { return err } - return db.WrapError( - d.db.TransactionContext(ctx, func(tx *db.Tx) error { - return d.store.DeleteUserByUsername(ctx, tx, username) - }), - ) + return d.db.TransactionContext(ctx, func(tx *db.Tx) error { + if err := d.store.DeleteUserByUsername(ctx, tx, username); err != nil { + return db.WrapError(err) + } + + return d.DeleteUserRepositories(ctx, username) + }) } // RemovePublicKey removes a public key from a user. diff --git a/server/db/migrate/0004_repo_owner.go b/server/db/migrate/0004_repo_owner.go new file mode 100644 index 000000000..07ab4598b --- /dev/null +++ b/server/db/migrate/0004_repo_owner.go @@ -0,0 +1,23 @@ +package migrate + +import ( + "context" + + "github.com/charmbracelet/soft-serve/server/db" +) + +const ( + repoOwnerName = "repo owner" + repoOwnerVersion = 4 +) + +var repoOwner = Migration{ + Version: repoOwnerVersion, + Name: repoOwnerName, + Migrate: func(ctx context.Context, tx *db.Tx) error { + return migrateUp(ctx, tx, repoOwnerVersion, repoOwnerName) + }, + Rollback: func(ctx context.Context, tx *db.Tx) error { + return migrateDown(ctx, tx, repoOwnerVersion, repoOwnerName) + }, +} diff --git a/server/db/migrate/0004_repo_owner_postgres.down.sql b/server/db/migrate/0004_repo_owner_postgres.down.sql new file mode 100644 index 000000000..cefbad5d5 --- /dev/null +++ b/server/db/migrate/0004_repo_owner_postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE repos DROP COLUMN user_id; diff --git a/server/db/migrate/0004_repo_owner_postgres.up.sql b/server/db/migrate/0004_repo_owner_postgres.up.sql new file mode 100644 index 000000000..c0a73d252 --- /dev/null +++ b/server/db/migrate/0004_repo_owner_postgres.up.sql @@ -0,0 +1,14 @@ +ALTER TABLE repos ADD COLUMN user_id INTEGER; + +UPDATE repos SET user_id = ( + SELECT id FROM users WHERE admin = true ORDER BY id LIMIT 1 +); + +ALTER TABLE repos +ALTER COLUMN user_id SET NOT NULL; + +ALTER TABLE repos +ADD CONSTRAINT user_id_fk +FOREIGN KEY(user_id) REFERENCES users(id) +ON DELETE CASCADE +ON UPDATE CASCADE; diff --git a/server/db/migrate/0004_repo_owner_sqlite.down.sql b/server/db/migrate/0004_repo_owner_sqlite.down.sql new file mode 100644 index 000000000..785e28f61 --- /dev/null +++ b/server/db/migrate/0004_repo_owner_sqlite.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS repos_old; diff --git a/server/db/migrate/0004_repo_owner_sqlite.up.sql b/server/db/migrate/0004_repo_owner_sqlite.up.sql new file mode 100644 index 000000000..453dbe85c --- /dev/null +++ b/server/db/migrate/0004_repo_owner_sqlite.up.sql @@ -0,0 +1,25 @@ +ALTER TABLE repos RENAME TO repos_old; + +CREATE TABLE repos ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + project_name TEXT NOT NULL, + description TEXT NOT NULL, + private BOOLEAN NOT NULL, + mirror BOOLEAN NOT NULL, + hidden BOOLEAN NOT NULL, + user_id INTEGER NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL, + CONSTRAINT user_id_fk + FOREIGN KEY(user_id) REFERENCES users(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +INSERT INTO repos (id, name, project_name, description, private, mirror, hidden, user_id, created_at, updated_at) +SELECT id, name, project_name, description, private, mirror, hidden, ( + SELECT id FROM users WHERE admin = true ORDER BY id LIMIT 1 +), created_at, updated_at +FROM repos_old; + diff --git a/server/db/migrate/migrations.go b/server/db/migrate/migrations.go index 987a71105..482d61cad 100644 --- a/server/db/migrate/migrations.go +++ b/server/db/migrate/migrations.go @@ -18,6 +18,7 @@ var migrations = []Migration{ createTables, createLFSTables, passwordTokens, + repoOwner, } func execMigration(ctx context.Context, tx *db.Tx, version int, name string, down bool) error { diff --git a/server/db/models/repo.go b/server/db/models/repo.go index 586a4f288..88300bd3e 100644 --- a/server/db/models/repo.go +++ b/server/db/models/repo.go @@ -1,16 +1,20 @@ package models -import "time" +import ( + "database/sql" + "time" +) // Repo is a database model for a repository. type Repo struct { - ID int64 `db:"id"` - Name string `db:"name"` - ProjectName string `db:"project_name"` - Description string `db:"description"` - Private bool `db:"private"` - Mirror bool `db:"mirror"` - Hidden bool `db:"hidden"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` + ID int64 `db:"id"` + Name string `db:"name"` + ProjectName string `db:"project_name"` + Description string `db:"description"` + Private bool `db:"private"` + Mirror bool `db:"mirror"` + Hidden bool `db:"hidden"` + UserID sql.NullInt64 `db:"user_id"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } diff --git a/server/git/lfs.go b/server/git/lfs.go index 1a1ead5a1..9e6851f19 100644 --- a/server/git/lfs.go +++ b/server/git/lfs.go @@ -85,6 +85,7 @@ func LFSTransfer(ctx context.Context, cmd ServiceCommand) error { return err } + repoID := strconv.FormatInt(repo.ID(), 10) cfg := config.FromContext(ctx) processor := transfer.NewProcessor(handler, &lfsTransfer{ ctx: ctx, @@ -92,7 +93,7 @@ func LFSTransfer(ctx context.Context, cmd ServiceCommand) error { dbx: db.FromContext(ctx), store: store.FromContext(ctx), logger: logger, - storage: storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs")), + storage: storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs", repoID)), repo: repo, }) @@ -132,7 +133,8 @@ func (t *lfsTransfer) Batch(_ string, pointers []transfer.Pointer, _ map[string] // Download implements transfer.Backend. func (t *lfsTransfer) Download(oid string, _ map[string]string) (fs.File, error) { cfg := config.FromContext(t.ctx) - strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs")) + repoID := strconv.FormatInt(t.repo.ID(), 10) + strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs", repoID)) pointer := transfer.Pointer{Oid: oid} return strg.Open(path.Join("objects", pointer.RelativePath())) } diff --git a/server/proto/repo.go b/server/proto/repo.go index 2d54e9484..ee6ccae29 100644 --- a/server/proto/repo.go +++ b/server/proto/repo.go @@ -22,6 +22,9 @@ type Repository interface { IsMirror() bool // IsHidden returns whether the repository is hidden. IsHidden() bool + // UserID returns the ID of the user who owns the repository. + // It returns 0 if the repository is not owned by a user. + UserID() int64 // UpdatedAt returns the time the repository was last updated. // If the repository has never been updated, it returns the time it was created. UpdatedAt() time.Time @@ -36,4 +39,6 @@ type RepositoryOptions struct { ProjectName string Mirror bool Hidden bool + LFS bool + LFSEndpoint string } diff --git a/server/ssh/cmd/create.go b/server/ssh/cmd/create.go index 751c18e14..7e301b3fd 100644 --- a/server/ssh/cmd/create.go +++ b/server/ssh/cmd/create.go @@ -21,8 +21,9 @@ func createCommand() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() be := backend.FromContext(ctx) + user := proto.UserFromContext(ctx) name := args[0] - if _, err := be.CreateRepository(ctx, name, proto.RepositoryOptions{ + if _, err := be.CreateRepository(ctx, name, user, proto.RepositoryOptions{ Private: private, Description: description, ProjectName: projectName, diff --git a/server/ssh/cmd/import.go b/server/ssh/cmd/import.go index 55f73e4f5..b34b46f61 100644 --- a/server/ssh/cmd/import.go +++ b/server/ssh/cmd/import.go @@ -13,6 +13,8 @@ func importCommand() *cobra.Command { var projectName string var mirror bool var hidden bool + var lfs bool + var lfsEndpoint string cmd := &cobra.Command{ Use: "import REPOSITORY REMOTE", @@ -22,14 +24,17 @@ func importCommand() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() be := backend.FromContext(ctx) + user := proto.UserFromContext(ctx) name := args[0] remote := args[1] - if _, err := be.ImportRepository(ctx, name, remote, proto.RepositoryOptions{ + if _, err := be.ImportRepository(ctx, name, user, remote, proto.RepositoryOptions{ Private: private, Description: description, ProjectName: projectName, Mirror: mirror, Hidden: hidden, + LFS: lfs, + LFSEndpoint: lfsEndpoint, }); err != nil { return err } @@ -37,6 +42,8 @@ func importCommand() *cobra.Command { }, } + cmd.Flags().BoolVarP(&lfs, "lfs", "", false, "pull Git LFS objects") + cmd.Flags().StringVarP(&lfsEndpoint, "lfs-endpoint", "", "", "set the Git LFS endpoint") cmd.Flags().BoolVarP(&mirror, "mirror", "m", false, "mirror the repository") cmd.Flags().BoolVarP(&private, "private", "p", false, "make the repository private") cmd.Flags().StringVarP(&description, "description", "d", "", "set the repository description") diff --git a/server/ssh/cmd/repo.go b/server/ssh/cmd/repo.go index 24f4b8c06..fc23844fb 100644 --- a/server/ssh/cmd/repo.go +++ b/server/ssh/cmd/repo.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/charmbracelet/soft-serve/server/backend" + "github.com/charmbracelet/soft-serve/server/proto" "github.com/spf13/cobra" ) @@ -59,6 +60,14 @@ func repoCommand() *cobra.Command { return err } + var owner proto.User + if rr.UserID() > 0 { + owner, err = be.UserByID(ctx, rr.UserID()) + if err != nil { + return err + } + } + branches, _ := r.Branches() tags, _ := r.Tags() @@ -70,6 +79,9 @@ func repoCommand() *cobra.Command { cmd.Println("Private:", rr.IsPrivate()) cmd.Println("Hidden:", rr.IsHidden()) cmd.Println("Mirror:", rr.IsMirror()) + if owner != nil { + cmd.Println(strings.TrimSpace(fmt.Sprint("Owner: ", owner.Username()))) + } cmd.Println("Default Branch:", head.Name().Short()) if len(branches) > 0 { cmd.Println("Branches:") diff --git a/server/ssh/git.go b/server/ssh/git.go index eac357591..b1a269a49 100644 --- a/server/ssh/git.go +++ b/server/ssh/git.go @@ -85,7 +85,7 @@ func handleGit(s ssh.Session) { return } if repo == nil { - if _, err := be.CreateRepository(ctx, name, proto.RepositoryOptions{Private: false}); err != nil { + if _, err := be.CreateRepository(ctx, name, user, proto.RepositoryOptions{Private: false}); err != nil { log.Errorf("failed to create repo: %s", err) sshFatal(s, err) return diff --git a/server/store/database/repo.go b/server/store/database/repo.go index 06ea92b49..03c080444 100644 --- a/server/store/database/repo.go +++ b/server/store/database/repo.go @@ -14,12 +14,21 @@ type repoStore struct{} var _ store.RepositoryStore = (*repoStore)(nil) // CreateRepo implements store.RepositoryStore. -func (*repoStore) CreateRepo(ctx context.Context, tx db.Handler, name string, projectName string, description string, isPrivate bool, isHidden bool, isMirror bool) error { +func (*repoStore) CreateRepo(ctx context.Context, tx db.Handler, name string, userID int64, projectName string, description string, isPrivate bool, isHidden bool, isMirror bool) error { name = utils.SanitizeRepo(name) - query := tx.Rebind(`INSERT INTO repos (name, project_name, description, private, mirror, hidden, updated_at) - VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP);`) - _, err := tx.ExecContext(ctx, query, - name, projectName, description, isPrivate, isMirror, isHidden) + values := []interface{}{ + name, projectName, description, isPrivate, isMirror, isHidden, + } + query := `INSERT INTO repos (name, project_name, description, private, mirror, hidden, updated_at) + VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP);` + if userID > 0 { + query = `INSERT INTO repos (name, project_name, description, private, mirror, hidden, updated_at, user_id) + VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, ?);` + values = append(values, userID) + } + + query = tx.Rebind(query) + _, err := tx.ExecContext(ctx, query, values...) return db.WrapError(err) } @@ -39,6 +48,14 @@ func (*repoStore) GetAllRepos(ctx context.Context, tx db.Handler) ([]models.Repo return repos, db.WrapError(err) } +// GetUserRepos implements store.RepositoryStore. +func (*repoStore) GetUserRepos(ctx context.Context, tx db.Handler, userID int64) ([]models.Repo, error) { + var repos []models.Repo + query := tx.Rebind("SELECT * FROM repos WHERE user_id = ?;") + err := tx.SelectContext(ctx, &repos, query, userID) + return repos, db.WrapError(err) +} + // GetRepoByName implements store.RepositoryStore. func (*repoStore) GetRepoByName(ctx context.Context, tx db.Handler, name string) (models.Repo, error) { var repo models.Repo diff --git a/server/store/repo.go b/server/store/repo.go index c64eb08ee..01feece79 100644 --- a/server/store/repo.go +++ b/server/store/repo.go @@ -11,7 +11,8 @@ import ( type RepositoryStore interface { GetRepoByName(ctx context.Context, h db.Handler, name string) (models.Repo, error) GetAllRepos(ctx context.Context, h db.Handler) ([]models.Repo, error) - CreateRepo(ctx context.Context, h db.Handler, name string, projectName string, description string, isPrivate bool, isHidden bool, isMirror bool) error + GetUserRepos(ctx context.Context, h db.Handler, userID int64) ([]models.Repo, error) + CreateRepo(ctx context.Context, h db.Handler, name string, userID int64, projectName string, description string, isPrivate bool, isHidden bool, isMirror bool) error DeleteRepoByName(ctx context.Context, h db.Handler, name string) error SetRepoNameByName(ctx context.Context, h db.Handler, name string, newName string) error diff --git a/server/web/git.go b/server/web/git.go index bf2fbd6e3..d2d640c5f 100644 --- a/server/web/git.go +++ b/server/web/git.go @@ -262,7 +262,7 @@ func withAccess(next http.Handler) http.HandlerFunc { // Create the repo if it doesn't exist. if repo == nil { - repo, err = be.CreateRepository(ctx, repoName, proto.RepositoryOptions{}) + repo, err = be.CreateRepository(ctx, repoName, user, proto.RepositoryOptions{}) if err != nil { logger.Error("failed to create repository", "repo", repoName, "err", err) renderInternalServerError(w, r) diff --git a/server/web/git_lfs.go b/server/web/git_lfs.go index b00a2661b..d73f5a442 100644 --- a/server/web/git_lfs.go +++ b/server/web/git_lfs.go @@ -89,7 +89,8 @@ func serviceLfsBatch(w http.ResponseWriter, r *http.Request) { dbx := db.FromContext(ctx) datastore := store.FromContext(ctx) // TODO: support S3 storage - strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs")) + repoID := strconv.FormatInt(repo.ID(), 10) + strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs", repoID)) baseHref := fmt.Sprintf("%s/%s/info/lfs/objects/basic", cfg.HTTP.PublicURL, name+".git") @@ -257,7 +258,8 @@ func serviceLfsBasicDownload(w http.ResponseWriter, r *http.Request) { logger := log.FromContext(ctx).WithPrefix("http.lfs-basic") datastore := store.FromContext(ctx) dbx := db.FromContext(ctx) - strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs")) + repoID := strconv.FormatInt(repo.ID(), 10) + strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs", repoID)) obj, err := datastore.GetLFSObjectByOid(ctx, dbx, repo.ID(), oid) if err != nil && !errors.Is(err, db.ErrRecordNotFound) { @@ -306,7 +308,9 @@ func serviceLfsBasicUpload(w http.ResponseWriter, r *http.Request) { dbx := db.FromContext(ctx) datastore := store.FromContext(ctx) logger := log.FromContext(ctx).WithPrefix("http.lfs-basic") - strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs")) + repo := proto.RepositoryFromContext(ctx) + repoID := strconv.FormatInt(repo.ID(), 10) + strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs", repoID)) name := mux.Vars(r)["repo"] defer r.Body.Close() // nolint: errcheck @@ -393,7 +397,8 @@ func serviceLfsBasicVerify(w http.ResponseWriter, r *http.Request) { cfg := config.FromContext(ctx) dbx := db.FromContext(ctx) datastore := store.FromContext(ctx) - strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs")) + repoID := strconv.FormatInt(repo.ID(), 10) + strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs", repoID)) if stat, err := strg.Stat(path.Join("objects", pointer.RelativePath())); err == nil { // Verify object is in the database. obj, err := datastore.GetLFSObjectByOid(ctx, dbx, repo.ID(), pointer.Oid) diff --git a/testscript/testdata/mirror.txtar b/testscript/testdata/mirror.txtar index 77b4bc281..e1db9214d 100644 --- a/testscript/testdata/mirror.txtar +++ b/testscript/testdata/mirror.txtar @@ -82,6 +82,7 @@ Description: Private: false Hidden: false Mirror: true +Owner: admin Default Branch: main Branches: - main @@ -92,6 +93,7 @@ Description: testing repo Private: true Hidden: true Mirror: true +Owner: admin Default Branch: main Branches: - main diff --git a/testscript/testdata/repo-create.txtar b/testscript/testdata/repo-create.txtar index 64849215c..6787ee9d0 100644 --- a/testscript/testdata/repo-create.txtar +++ b/testscript/testdata/repo-create.txtar @@ -90,6 +90,27 @@ soft repo branch delete repo1 master soft repo branch list repo1 stdout branch1 +# create a new user +soft user create bar --key "$USER1_AUTHORIZED_KEY" + +# user create a repo +usoft repo create repo2 -d 'description' -H -p -n 'repo2' +usoft repo hidden repo2 +stdout true +usoft repo private repo2 +stdout true +! exists $DATA_PATH/repos/repo2.git/git-daemon-export-ok +usoft repo description repo2 +stdout 'description' +readfile $DATA_PATH/repos/repo2.git/description 'description' +usoft repo project-name repo2 +stdout 'repo2' + +# user delete a repo +usoft repo delete repo2 +! exists $DATA_PATH/repos/repo2.git + + -- readme.md -- # Project\nfoo -- branch_list.1.txt -- @@ -102,6 +123,7 @@ Description: description Private: true Hidden: true Mirror: false +Owner: admin Default Branch: master Branches: - master diff --git a/testscript/testdata/repo-import.txtar b/testscript/testdata/repo-import.txtar index 4d73a5a43..d85cdf379 100644 --- a/testscript/testdata/repo-import.txtar +++ b/testscript/testdata/repo-import.txtar @@ -25,6 +25,7 @@ Description: descriptive Private: false Hidden: false Mirror: false +Owner: admin Default Branch: main Branches: - main diff --git a/testscript/testdata/repo-perms.txtar b/testscript/testdata/repo-perms.txtar index bb3e423ac..cf21151b5 100644 --- a/testscript/testdata/repo-perms.txtar +++ b/testscript/testdata/repo-perms.txtar @@ -82,6 +82,7 @@ Description: desc Private: true Hidden: false Mirror: false +Owner: admin Default Branch: master Branches: - master