Detailed changes
@@ -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,
@@ -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)
@@ -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.
@@ -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.
@@ -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)
+ },
+}
@@ -0,0 +1 @@
+ALTER TABLE repos DROP COLUMN user_id;
@@ -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;
@@ -0,0 +1 @@
+DROP TABLE IF EXISTS repos_old;
@@ -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;
+
@@ -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 {
@@ -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"`
}
@@ -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()))
}
@@ -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
}
@@ -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,
@@ -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")
@@ -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:")
@@ -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
@@ -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
@@ -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
@@ -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)
@@ -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)
@@ -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
@@ -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
@@ -25,6 +25,7 @@ Description: descriptive
Private: false
Hidden: false
Mirror: false
+Owner: admin
Default Branch: main
Branches:
- main
@@ -82,6 +82,7 @@ Description: desc
Private: true
Hidden: false
Mirror: false
+Owner: admin
Default Branch: master
Branches:
- master