ea6b9a46c37dd6323c77670bd27bd8341db8ac08
- Author
- Ayman Bagabas <ayman.bagabas@gmail.com>
- Committer
- Ayman Bagabas <ayman.bagabas@gmail.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/cmd/soft/migrate_config.go b/cmd/soft/migrate_config.go
2index 4d85c0bd82accb9ca38600b29b1352ea4027a36f..583153e5d84e1bbd3e73538267ed6bf85f0d36cb 100644
3--- a/cmd/soft/migrate_config.go
4+++ b/cmd/soft/migrate_config.go
5@@ -177,7 +177,7 @@ var migrateConfig = &cobra.Command{
6 return fmt.Errorf("failed to copy repo: %w", err)
7 }
8
9- if _, err := sb.CreateRepository(ctx, dir.Name(), proto.RepositoryOptions{}); err != nil {
10+ if _, err := sb.CreateRepository(ctx, dir.Name(), nil, proto.RepositoryOptions{}); err != nil {
11 fmt.Fprintf(os.Stderr, "failed to create repository: %s\n", err)
12 }
13 }
14@@ -239,7 +239,7 @@ var migrateConfig = &cobra.Command{
15 }
16
17 // Create `.soft-serve` repository and add readme
18- if _, err := sb.CreateRepository(ctx, ".soft-serve", proto.RepositoryOptions{
19+ if _, err := sb.CreateRepository(ctx, ".soft-serve", nil, proto.RepositoryOptions{
20 ProjectName: "Home",
21 Description: "Soft Serve home repository",
22 Hidden: true,
23diff --git a/server/backend/lfs.go b/server/backend/lfs.go
24index eac557ecf07fee83ba8fef0216a8e1074e36e4e4..5b0d5a4064ae92d3fe1044ef8a841bbb8e9663a1 100644
25--- a/server/backend/lfs.go
26+++ b/server/backend/lfs.go
27@@ -6,6 +6,7 @@ import (
28 "io"
29 "path"
30 "path/filepath"
31+ "strconv"
32
33 "github.com/charmbracelet/soft-serve/server/config"
34 "github.com/charmbracelet/soft-serve/server/db"
35@@ -18,7 +19,8 @@ import (
36 // StoreRepoMissingLFSObjects stores missing LFS objects for a repository.
37 func StoreRepoMissingLFSObjects(ctx context.Context, repo proto.Repository, dbx *db.DB, store store.Store, lfsClient lfs.Client) error {
38 cfg := config.FromContext(ctx)
39- lfsRoot := filepath.Join(cfg.DataPath, "lfs")
40+ repoID := strconv.FormatInt(repo.ID(), 10)
41+ lfsRoot := filepath.Join(cfg.DataPath, "lfs", repoID)
42
43 // TODO: support S3 storage
44 strg := storage.NewLocalStorage(lfsRoot)
45diff --git a/server/backend/repo.go b/server/backend/repo.go
46index 840385112d5703055ac7ce73087ae55b959fd9d7..b31484ba66eae1bc987d7d312445f085bc08f749 100644
47--- a/server/backend/repo.go
48+++ b/server/backend/repo.go
49@@ -7,8 +7,10 @@ import (
50 "fmt"
51 "io/fs"
52 "os"
53+ "os/exec"
54 "path"
55 "path/filepath"
56+ "strconv"
57 "time"
58
59 "github.com/charmbracelet/soft-serve/git"
60@@ -28,7 +30,7 @@ func (d *Backend) reposPath() string {
61 // CreateRepository creates a new repository.
62 //
63 // It implements backend.Backend.
64-func (d *Backend) CreateRepository(ctx context.Context, name string, opts proto.RepositoryOptions) (proto.Repository, error) {
65+func (d *Backend) CreateRepository(ctx context.Context, name string, user proto.User, opts proto.RepositoryOptions) (proto.Repository, error) {
66 name = utils.SanitizeRepo(name)
67 if err := utils.ValidateRepo(name); err != nil {
68 return nil, err
69@@ -37,11 +39,17 @@ func (d *Backend) CreateRepository(ctx context.Context, name string, opts proto.
70 repo := name + ".git"
71 rp := filepath.Join(d.reposPath(), repo)
72
73+ var userID int64
74+ if user != nil {
75+ userID = user.ID()
76+ }
77+
78 if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
79 if err := d.store.CreateRepo(
80 ctx,
81 tx,
82 name,
83+ userID,
84 opts.ProjectName,
85 opts.Description,
86 opts.Private,
87@@ -72,14 +80,19 @@ func (d *Backend) CreateRepository(ctx context.Context, name string, opts proto.
88 return hooks.GenerateHooks(ctx, d.cfg, repo)
89 }); err != nil {
90 d.logger.Debug("failed to create repository in database", "err", err)
91- return nil, db.WrapError(err)
92+ err = db.WrapError(err)
93+ if errors.Is(err, db.ErrDuplicateKey) {
94+ return nil, proto.ErrRepoExist
95+ }
96+
97+ return nil, err
98 }
99
100 return d.Repository(ctx, name)
101 }
102
103 // ImportRepository imports a repository from remote.
104-func (d *Backend) ImportRepository(ctx context.Context, name string, remote string, opts proto.RepositoryOptions) (proto.Repository, error) {
105+func (d *Backend) ImportRepository(ctx context.Context, name string, user proto.User, remote string, opts proto.RepositoryOptions) (proto.Repository, error) {
106 name = utils.SanitizeRepo(name)
107 if err := utils.ValidateRepo(name); err != nil {
108 return nil, err
109@@ -92,37 +105,42 @@ func (d *Backend) ImportRepository(ctx context.Context, name string, remote stri
110 return nil, proto.ErrRepoExist
111 }
112
113- copts := git.CloneOptions{
114- Bare: true,
115- Mirror: opts.Mirror,
116- Quiet: true,
117- CommandOptions: git.CommandOptions{
118- Timeout: -1,
119- Context: ctx,
120- Envs: []string{
121- fmt.Sprintf(`GIT_SSH_COMMAND=ssh -o UserKnownHostsFile="%s" -o StrictHostKeyChecking=no -i "%s"`,
122- filepath.Join(d.cfg.DataPath, "ssh", "known_hosts"),
123- d.cfg.SSH.ClientKeyPath,
124- ),
125- },
126- },
127+ if err := os.MkdirAll(rp, fs.ModePerm); err != nil {
128+ return nil, err
129 }
130
131- if err := git.Clone(remote, rp, copts); err != nil {
132+ cmd := exec.CommandContext(ctx, "git", "clone", "--bare", "--mirror", remote, ".")
133+ cmd.Env = append(cmd.Env,
134+ fmt.Sprintf(`GIT_SSH_COMMAND=ssh -o UserKnownHostsFile="%s" -o StrictHostKeyChecking=no -i "%s"`,
135+ filepath.Join(d.cfg.DataPath, "ssh", "known_hosts"),
136+ d.cfg.SSH.ClientKeyPath,
137+ ),
138+ )
139+ cmd.Dir = rp
140+ if err := cmd.Run(); err != nil {
141 d.logger.Error("failed to clone repository", "err", err, "mirror", opts.Mirror, "remote", remote, "path", rp)
142 // Cleanup the mess!
143 if rerr := os.RemoveAll(rp); rerr != nil {
144 err = errors.Join(err, rerr)
145 }
146+
147 return nil, err
148 }
149diff --git a/server/backend/user.go b/server/backend/user.go
150index 8f1f4bb73c3c3cf3ac4a3b1f5ef8130a93cca846..07db904d55d26d8d0cbc28407d0cb4de7960298c 100644
151--- a/server/backend/user.go
152+++ b/server/backend/user.go
153@@ -62,6 +62,13 @@ func (d *Backend) AccessLevelForUser(ctx context.Context, repo string, user prot
154 }
155
156 if r != nil {
157+ if user != nil {
158+ // If the user is the owner, they have admin access.
159+ if r.UserID() == user.ID() {
160+ return access.AdminAccess
161+ }
162+ }
163+
164 // If the user is a collaborator, they have read/write access.
165 isCollab, _ := d.IsCollaborator(ctx, repo, username)
166 if isCollab {
167@@ -128,6 +135,34 @@ func (d *Backend) User(ctx context.Context, username string) (proto.User, error)
168 }, nil
169 }
170
171+// UserByID finds a user by ID.
172+func (d *Backend) UserByID(ctx context.Context, id int64) (proto.User, error) {
173+ var m models.User
174+ var pks []ssh.PublicKey
175+ if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
176+ var err error
177+ m, err = d.store.GetUserByID(ctx, tx, id)
178+ if err != nil {
179+ return err
180+ }
181+
182+ pks, err = d.store.ListPublicKeysByUserID(ctx, tx, m.ID)
183+ return err
184+ }); err != nil {
185+ err = db.WrapError(err)
186+ if errors.Is(err, db.ErrRecordNotFound) {
187+ return nil, proto.ErrUserNotFound
188+ }
189+ d.logger.Error("error finding user", "id", id, "error", err)
190+ return nil, err
191+ }
192+
193+ return &user{
194+ user: m,
195+ publicKeys: pks,
196+ }, nil
197+}
198+
199 // UserByPublicKey finds a user by public key.
200 //
201 // It implements backend.Backend.
202@@ -263,11 +298,13 @@ func (d *Backend) DeleteUser(ctx context.Context, username string) error {
203 return err
204 }
205
206- return db.WrapError(
207- d.db.TransactionContext(ctx, func(tx *db.Tx) error {
208- return d.store.DeleteUserByUsername(ctx, tx, username)
209- }),
210- )
211+ return d.db.TransactionContext(ctx, func(tx *db.Tx) error {
212+ if err := d.store.DeleteUserByUsername(ctx, tx, username); err != nil {
213+ return db.WrapError(err)
214+ }
215+
216+ return d.DeleteUserRepositories(ctx, username)
217+ })
218 }
219
220 // RemovePublicKey removes a public key from a user.
221diff --git a/server/db/migrate/0004_repo_owner.go b/server/db/migrate/0004_repo_owner.go
222new file mode 100644
223index 0000000000000000000000000000000000000000..07ab4598b3baba6e0dfecd6f0ce329508ca3ee90
224--- /dev/null
225+++ b/server/db/migrate/0004_repo_owner.go
226@@ -0,0 +1,23 @@
227+package migrate
228+
229+import (
230+ "context"
231+
232+ "github.com/charmbracelet/soft-serve/server/db"
233+)
234+
235+const (
236+ repoOwnerName = "repo owner"
237+ repoOwnerVersion = 4
238+)
239+
240+var repoOwner = Migration{
241+ Version: repoOwnerVersion,
242+ Name: repoOwnerName,
243+ Migrate: func(ctx context.Context, tx *db.Tx) error {
244+ return migrateUp(ctx, tx, repoOwnerVersion, repoOwnerName)
245+ },
246+ Rollback: func(ctx context.Context, tx *db.Tx) error {
247+ return migrateDown(ctx, tx, repoOwnerVersion, repoOwnerName)
248+ },
249+}
250diff --git a/server/db/migrate/0004_repo_owner_postgres.down.sql b/server/db/migrate/0004_repo_owner_postgres.down.sql
251new file mode 100644
252index 0000000000000000000000000000000000000000..cefbad5d53b9330bd98291c2f0853ff5a3b7a5ce
253--- /dev/null
254+++ b/server/db/migrate/0004_repo_owner_postgres.down.sql
255@@ -0,0 +1 @@
256+ALTER TABLE repos DROP COLUMN user_id;
257diff --git a/server/db/migrate/0004_repo_owner_postgres.up.sql b/server/db/migrate/0004_repo_owner_postgres.up.sql
258new file mode 100644
259index 0000000000000000000000000000000000000000..c0a73d252849562eb111a90fb74a27e6954a1179
260--- /dev/null
261+++ b/server/db/migrate/0004_repo_owner_postgres.up.sql
262@@ -0,0 +1,14 @@
263+ALTER TABLE repos ADD COLUMN user_id INTEGER;
264+
265+UPDATE repos SET user_id = (
266+ SELECT id FROM users WHERE admin = true ORDER BY id LIMIT 1
267+);
268+
269+ALTER TABLE repos
270+ALTER COLUMN user_id SET NOT NULL;
271+
272+ALTER TABLE repos
273+ADD CONSTRAINT user_id_fk
274+FOREIGN KEY(user_id) REFERENCES users(id)
275+ON DELETE CASCADE
276+ON UPDATE CASCADE;
277diff --git a/server/db/migrate/0004_repo_owner_sqlite.down.sql b/server/db/migrate/0004_repo_owner_sqlite.down.sql
278new file mode 100644
279index 0000000000000000000000000000000000000000..785e28f6188b03635cf9ba6ae0c84e118a0c81eb
280--- /dev/null
281+++ b/server/db/migrate/0004_repo_owner_sqlite.down.sql
282@@ -0,0 +1 @@
283+DROP TABLE IF EXISTS repos_old;
284diff --git a/server/db/migrate/0004_repo_owner_sqlite.up.sql b/server/db/migrate/0004_repo_owner_sqlite.up.sql
285new file mode 100644
286index 0000000000000000000000000000000000000000..453dbe85ca9f5fc76a7772629031a1eeb25eebeb
287--- /dev/null
288+++ b/server/db/migrate/0004_repo_owner_sqlite.up.sql
289@@ -0,0 +1,25 @@
290+ALTER TABLE repos RENAME TO repos_old;
291+
292+CREATE TABLE repos (
293+ id INTEGER PRIMARY KEY AUTOINCREMENT,
294+ name TEXT NOT NULL UNIQUE,
295+ project_name TEXT NOT NULL,
296+ description TEXT NOT NULL,
297+ private BOOLEAN NOT NULL,
298+ mirror BOOLEAN NOT NULL,
299+ hidden BOOLEAN NOT NULL,
300+ user_id INTEGER NOT NULL,
301+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
302+ updated_at DATETIME NOT NULL,
303+ CONSTRAINT user_id_fk
304+ FOREIGN KEY(user_id) REFERENCES users(id)
305+ ON DELETE CASCADE
306+ ON UPDATE CASCADE
307+);
308+
309+INSERT INTO repos (id, name, project_name, description, private, mirror, hidden, user_id, created_at, updated_at)
310+SELECT id, name, project_name, description, private, mirror, hidden, (
311+ SELECT id FROM users WHERE admin = true ORDER BY id LIMIT 1
312+), created_at, updated_at
313+FROM repos_old;
314+
315diff --git a/server/db/migrate/migrations.go b/server/db/migrate/migrations.go
316index 987a71105c0b51651a1d6151e790a16069aa1d21..482d61cad2b3e1e5a12e341264cbe43fc621d04a 100644
317--- a/server/db/migrate/migrations.go
318+++ b/server/db/migrate/migrations.go
319@@ -18,6 +18,7 @@ var migrations = []Migration{
320 createTables,
321 createLFSTables,
322 passwordTokens,
323+ repoOwner,
324 }
325
326 func execMigration(ctx context.Context, tx *db.Tx, version int, name string, down bool) error {
327diff --git a/server/db/models/repo.go b/server/db/models/repo.go
328index 586a4f288941aa8298e1e3a527d6ec6d3cddc07b..88300bd3e3634822c7f39d928e1ad1bd474e74e9 100644
329--- a/server/db/models/repo.go
330+++ b/server/db/models/repo.go
331@@ -1,16 +1,20 @@
332 package models
333
334-import "time"
335+import (
336+ "database/sql"
337+ "time"
338+)
339
340 // Repo is a database model for a repository.
341 type Repo struct {
342- ID int64 `db:"id"`
343- Name string `db:"name"`
344- ProjectName string `db:"project_name"`
345- Description string `db:"description"`
346- Private bool `db:"private"`
347- Mirror bool `db:"mirror"`
348- Hidden bool `db:"hidden"`
349- CreatedAt time.Time `db:"created_at"`
350- UpdatedAt time.Time `db:"updated_at"`
351+ ID int64 `db:"id"`
352+ Name string `db:"name"`
353+ ProjectName string `db:"project_name"`
354+ Description string `db:"description"`
355+ Private bool `db:"private"`
356+ Mirror bool `db:"mirror"`
357+ Hidden bool `db:"hidden"`
358+ UserID sql.NullInt64 `db:"user_id"`
359+ CreatedAt time.Time `db:"created_at"`
360+ UpdatedAt time.Time `db:"updated_at"`
361 }
362diff --git a/server/git/lfs.go b/server/git/lfs.go
363index 1a1ead5a1ffb65cd18f084fc17a702c036e7e4b4..9e6851f1965a7051aeca7854870c4ec84aa9e711 100644
364--- a/server/git/lfs.go
365+++ b/server/git/lfs.go
366@@ -85,6 +85,7 @@ func LFSTransfer(ctx context.Context, cmd ServiceCommand) error {
367 return err
368 }
369
370+ repoID := strconv.FormatInt(repo.ID(), 10)
371 cfg := config.FromContext(ctx)
372 processor := transfer.NewProcessor(handler, &lfsTransfer{
373 ctx: ctx,
374@@ -92,7 +93,7 @@ func LFSTransfer(ctx context.Context, cmd ServiceCommand) error {
375 dbx: db.FromContext(ctx),
376 store: store.FromContext(ctx),
377 logger: logger,
378- storage: storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs")),
379+ storage: storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs", repoID)),
380 repo: repo,
381 })
382
383@@ -132,7 +133,8 @@ func (t *lfsTransfer) Batch(_ string, pointers []transfer.Pointer, _ map[string]
384 // Download implements transfer.Backend.
385 func (t *lfsTransfer) Download(oid string, _ map[string]string) (fs.File, error) {
386 cfg := config.FromContext(t.ctx)
387- strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs"))
388+ repoID := strconv.FormatInt(t.repo.ID(), 10)
389+ strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs", repoID))
390 pointer := transfer.Pointer{Oid: oid}
391 return strg.Open(path.Join("objects", pointer.RelativePath()))
392 }
393diff --git a/server/proto/repo.go b/server/proto/repo.go
394index 2d54e9484caf3ec38269be0907a4af075b93d69d..ee6ccae29d174e00cbab536b7141073a80510618 100644
395--- a/server/proto/repo.go
396+++ b/server/proto/repo.go
397@@ -22,6 +22,9 @@ type Repository interface {
398 IsMirror() bool
399 // IsHidden returns whether the repository is hidden.
400 IsHidden() bool
401+ // UserID returns the ID of the user who owns the repository.
402+ // It returns 0 if the repository is not owned by a user.
403+ UserID() int64
404 // UpdatedAt returns the time the repository was last updated.
405 // If the repository has never been updated, it returns the time it was created.
406 UpdatedAt() time.Time
407@@ -36,4 +39,6 @@ type RepositoryOptions struct {
408 ProjectName string
409 Mirror bool
410 Hidden bool
411+ LFS bool
412+ LFSEndpoint string
413 }
414diff --git a/server/ssh/cmd/create.go b/server/ssh/cmd/create.go
415index 751c18e1438eb03b23d4649499378c61679cd906..7e301b3fd014bd3a8d821d7b34667b609c50edfb 100644
416--- a/server/ssh/cmd/create.go
417+++ b/server/ssh/cmd/create.go
418@@ -21,8 +21,9 @@ func createCommand() *cobra.Command {
419 RunE: func(cmd *cobra.Command, args []string) error {
420 ctx := cmd.Context()
421 be := backend.FromContext(ctx)
422+ user := proto.UserFromContext(ctx)
423 name := args[0]
424- if _, err := be.CreateRepository(ctx, name, proto.RepositoryOptions{
425+ if _, err := be.CreateRepository(ctx, name, user, proto.RepositoryOptions{
426 Private: private,
427 Description: description,
428 ProjectName: projectName,
429diff --git a/server/ssh/cmd/import.go b/server/ssh/cmd/import.go
430index 55f73e4f5df78dfb21f41655bba96f6f04a8ba02..b34b46f613389ed41a779060549e2ac760d8c4b9 100644
431--- a/server/ssh/cmd/import.go
432+++ b/server/ssh/cmd/import.go
433@@ -13,6 +13,8 @@ func importCommand() *cobra.Command {
434 var projectName string
435 var mirror bool
436 var hidden bool
437+ var lfs bool
438+ var lfsEndpoint string
439
440 cmd := &cobra.Command{
441 Use: "import REPOSITORY REMOTE",
442@@ -22,14 +24,17 @@ func importCommand() *cobra.Command {
443 RunE: func(cmd *cobra.Command, args []string) error {
444 ctx := cmd.Context()
445 be := backend.FromContext(ctx)
446+ user := proto.UserFromContext(ctx)
447 name := args[0]
448 remote := args[1]
449- if _, err := be.ImportRepository(ctx, name, remote, proto.RepositoryOptions{
450+ if _, err := be.ImportRepository(ctx, name, user, remote, proto.RepositoryOptions{
451 Private: private,
452 Description: description,
453 ProjectName: projectName,
454 Mirror: mirror,
455 Hidden: hidden,
456+ LFS: lfs,
457+ LFSEndpoint: lfsEndpoint,
458 }); err != nil {
459 return err
460 }
461@@ -37,6 +42,8 @@ func importCommand() *cobra.Command {
462 },
463 }
464
465+ cmd.Flags().BoolVarP(&lfs, "lfs", "", false, "pull Git LFS objects")
466+ cmd.Flags().StringVarP(&lfsEndpoint, "lfs-endpoint", "", "", "set the Git LFS endpoint")
467 cmd.Flags().BoolVarP(&mirror, "mirror", "m", false, "mirror the repository")
468 cmd.Flags().BoolVarP(&private, "private", "p", false, "make the repository private")
469 cmd.Flags().StringVarP(&description, "description", "d", "", "set the repository description")
470diff --git a/server/ssh/cmd/repo.go b/server/ssh/cmd/repo.go
471index 24f4b8c0622421b117346a349c1e2f38fb3de4d3..fc23844fb1494268d6f352091617cdca6f3d8c44 100644
472--- a/server/ssh/cmd/repo.go
473+++ b/server/ssh/cmd/repo.go
474@@ -5,6 +5,7 @@ import (
475 "strings"
476
477 "github.com/charmbracelet/soft-serve/server/backend"
478+ "github.com/charmbracelet/soft-serve/server/proto"
479 "github.com/spf13/cobra"
480 )
481
482@@ -59,6 +60,14 @@ func repoCommand() *cobra.Command {
483 return err
484 }
485
486+ var owner proto.User
487+ if rr.UserID() > 0 {
488+ owner, err = be.UserByID(ctx, rr.UserID())
489+ if err != nil {
490+ return err
491+ }
492+ }
493+
494 branches, _ := r.Branches()
495 tags, _ := r.Tags()
496
497@@ -70,6 +79,9 @@ func repoCommand() *cobra.Command {
498 cmd.Println("Private:", rr.IsPrivate())
499 cmd.Println("Hidden:", rr.IsHidden())
500 cmd.Println("Mirror:", rr.IsMirror())
501+ if owner != nil {
502+ cmd.Println(strings.TrimSpace(fmt.Sprint("Owner: ", owner.Username())))
503+ }
504 cmd.Println("Default Branch:", head.Name().Short())
505 if len(branches) > 0 {
506 cmd.Println("Branches:")
507diff --git a/server/ssh/git.go b/server/ssh/git.go
508index eac3575913dcfc614142b7e6bed5d0e423d1c87e..b1a269a4999fe21181d327bf372f763701ca172b 100644
509--- a/server/ssh/git.go
510+++ b/server/ssh/git.go
511@@ -85,7 +85,7 @@ func handleGit(s ssh.Session) {
512 return
513 }
514 if repo == nil {
515- if _, err := be.CreateRepository(ctx, name, proto.RepositoryOptions{Private: false}); err != nil {
516+ if _, err := be.CreateRepository(ctx, name, user, proto.RepositoryOptions{Private: false}); err != nil {
517 log.Errorf("failed to create repo: %s", err)
518 sshFatal(s, err)
519 return
520diff --git a/server/store/database/repo.go b/server/store/database/repo.go
521index 06ea92b492c32cfaf4c1020611daebf5ea7a8bbb..03c080444a035f0192f2ddfc103bf1eddd583c21 100644
522--- a/server/store/database/repo.go
523+++ b/server/store/database/repo.go
524@@ -14,12 +14,21 @@ type repoStore struct{}
525 var _ store.RepositoryStore = (*repoStore)(nil)
526
527 // CreateRepo implements store.RepositoryStore.
528-func (*repoStore) CreateRepo(ctx context.Context, tx db.Handler, name string, projectName string, description string, isPrivate bool, isHidden bool, isMirror bool) error {
529+func (*repoStore) CreateRepo(ctx context.Context, tx db.Handler, name string, userID int64, projectName string, description string, isPrivate bool, isHidden bool, isMirror bool) error {
530 name = utils.SanitizeRepo(name)
531- query := tx.Rebind(`INSERT INTO repos (name, project_name, description, private, mirror, hidden, updated_at)
532- VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP);`)
533- _, err := tx.ExecContext(ctx, query,
534- name, projectName, description, isPrivate, isMirror, isHidden)
535+ values := []interface{}{
536+ name, projectName, description, isPrivate, isMirror, isHidden,
537+ }
538+ query := `INSERT INTO repos (name, project_name, description, private, mirror, hidden, updated_at)
539+ VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP);`
540+ if userID > 0 {
541+ query = `INSERT INTO repos (name, project_name, description, private, mirror, hidden, updated_at, user_id)
542+ VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, ?);`
543+ values = append(values, userID)
544+ }
545+
546+ query = tx.Rebind(query)
547+ _, err := tx.ExecContext(ctx, query, values...)
548 return db.WrapError(err)
549 }
550
551@@ -39,6 +48,14 @@ func (*repoStore) GetAllRepos(ctx context.Context, tx db.Handler) ([]models.Repo
552 return repos, db.WrapError(err)
553 }
554
555+// GetUserRepos implements store.RepositoryStore.
556+func (*repoStore) GetUserRepos(ctx context.Context, tx db.Handler, userID int64) ([]models.Repo, error) {
557+ var repos []models.Repo
558+ query := tx.Rebind("SELECT * FROM repos WHERE user_id = ?;")
559+ err := tx.SelectContext(ctx, &repos, query, userID)
560+ return repos, db.WrapError(err)
561+}
562+
563 // GetRepoByName implements store.RepositoryStore.
564 func (*repoStore) GetRepoByName(ctx context.Context, tx db.Handler, name string) (models.Repo, error) {
565 var repo models.Repo
566diff --git a/server/store/repo.go b/server/store/repo.go
567index c64eb08ee491d46b173193908fb57e8a920f3598..01feece79cdfd608fda5ed77f04e8771fc4e46fe 100644
568--- a/server/store/repo.go
569+++ b/server/store/repo.go
570@@ -11,7 +11,8 @@ import (
571 type RepositoryStore interface {
572 GetRepoByName(ctx context.Context, h db.Handler, name string) (models.Repo, error)
573 GetAllRepos(ctx context.Context, h db.Handler) ([]models.Repo, error)
574- CreateRepo(ctx context.Context, h db.Handler, name string, projectName string, description string, isPrivate bool, isHidden bool, isMirror bool) error
575+ GetUserRepos(ctx context.Context, h db.Handler, userID int64) ([]models.Repo, error)
576+ CreateRepo(ctx context.Context, h db.Handler, name string, userID int64, projectName string, description string, isPrivate bool, isHidden bool, isMirror bool) error
577 DeleteRepoByName(ctx context.Context, h db.Handler, name string) error
578 SetRepoNameByName(ctx context.Context, h db.Handler, name string, newName string) error
579
580diff --git a/server/web/git.go b/server/web/git.go
581index bf2fbd6e37e9e7baad6b7890d0ab7c10b5fa0ff8..d2d640c5f730b0d4eec28c4cf09d6dcbc0e44f52 100644
582--- a/server/web/git.go
583+++ b/server/web/git.go
584@@ -262,7 +262,7 @@ func withAccess(next http.Handler) http.HandlerFunc {
585
586 // Create the repo if it doesn't exist.
587 if repo == nil {
588- repo, err = be.CreateRepository(ctx, repoName, proto.RepositoryOptions{})
589+ repo, err = be.CreateRepository(ctx, repoName, user, proto.RepositoryOptions{})
590 if err != nil {
591 logger.Error("failed to create repository", "repo", repoName, "err", err)
592 renderInternalServerError(w, r)
593diff --git a/server/web/git_lfs.go b/server/web/git_lfs.go
594index b00a2661bcd006927492533fc1b3c277b5a59d31..d73f5a44208a8a28a1c4624e81130918d2353909 100644
595--- a/server/web/git_lfs.go
596+++ b/server/web/git_lfs.go
597@@ -89,7 +89,8 @@ func serviceLfsBatch(w http.ResponseWriter, r *http.Request) {
598 dbx := db.FromContext(ctx)
599 datastore := store.FromContext(ctx)
600 // TODO: support S3 storage
601- strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs"))
602+ repoID := strconv.FormatInt(repo.ID(), 10)
603+ strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs", repoID))
604
605 baseHref := fmt.Sprintf("%s/%s/info/lfs/objects/basic", cfg.HTTP.PublicURL, name+".git")
606
607@@ -257,7 +258,8 @@ func serviceLfsBasicDownload(w http.ResponseWriter, r *http.Request) {
608 logger := log.FromContext(ctx).WithPrefix("http.lfs-basic")
609 datastore := store.FromContext(ctx)
610 dbx := db.FromContext(ctx)
611- strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs"))
612+ repoID := strconv.FormatInt(repo.ID(), 10)
613+ strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs", repoID))
614
615 obj, err := datastore.GetLFSObjectByOid(ctx, dbx, repo.ID(), oid)
616 if err != nil && !errors.Is(err, db.ErrRecordNotFound) {
617@@ -306,7 +308,9 @@ func serviceLfsBasicUpload(w http.ResponseWriter, r *http.Request) {
618 dbx := db.FromContext(ctx)
619 datastore := store.FromContext(ctx)
620 logger := log.FromContext(ctx).WithPrefix("http.lfs-basic")
621- strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs"))
622+ repo := proto.RepositoryFromContext(ctx)
623+ repoID := strconv.FormatInt(repo.ID(), 10)
624+ strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs", repoID))
625 name := mux.Vars(r)["repo"]
626
627 defer r.Body.Close() // nolint: errcheck
628@@ -393,7 +397,8 @@ func serviceLfsBasicVerify(w http.ResponseWriter, r *http.Request) {
629 cfg := config.FromContext(ctx)
630 dbx := db.FromContext(ctx)
631 datastore := store.FromContext(ctx)
632- strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs"))
633+ repoID := strconv.FormatInt(repo.ID(), 10)
634+ strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs", repoID))
635 if stat, err := strg.Stat(path.Join("objects", pointer.RelativePath())); err == nil {
636 // Verify object is in the database.
637 obj, err := datastore.GetLFSObjectByOid(ctx, dbx, repo.ID(), pointer.Oid)
638diff --git a/testscript/testdata/mirror.txtar b/testscript/testdata/mirror.txtar
639index 77b4bc281414c98620f0ba3b34cfc5e6447baa68..e1db9214d4f96288b5561b0ed8c41d413b5d66ff 100644
640--- a/testscript/testdata/mirror.txtar
641+++ b/testscript/testdata/mirror.txtar
642@@ -82,6 +82,7 @@ Description:
643 Private: false
644 Hidden: false
645 Mirror: true
646+Owner: admin
647 Default Branch: main
648 Branches:
649 - main
650@@ -92,6 +93,7 @@ Description: testing repo
651 Private: true
652 Hidden: true
653 Mirror: true
654+Owner: admin
655 Default Branch: main
656 Branches:
657 - main
658diff --git a/testscript/testdata/repo-create.txtar b/testscript/testdata/repo-create.txtar
659index 64849215c43d49b2ac9d420f29b3e6689aaf5b99..6787ee9d0b32828f5613bbf70adfe4b60dcbd66f 100644
660--- a/testscript/testdata/repo-create.txtar
661+++ b/testscript/testdata/repo-create.txtar
662@@ -90,6 +90,27 @@ soft repo branch delete repo1 master
663 soft repo branch list repo1
664 stdout branch1
665
666+# create a new user
667+soft user create bar --key "$USER1_AUTHORIZED_KEY"
668+
669+# user create a repo
670+usoft repo create repo2 -d 'description' -H -p -n 'repo2'
671+usoft repo hidden repo2
672+stdout true
673+usoft repo private repo2
674+stdout true
675+! exists $DATA_PATH/repos/repo2.git/git-daemon-export-ok
676+usoft repo description repo2
677+stdout 'description'
678+readfile $DATA_PATH/repos/repo2.git/description 'description'
679+usoft repo project-name repo2
680+stdout 'repo2'
681+
682+# user delete a repo
683+usoft repo delete repo2
684+! exists $DATA_PATH/repos/repo2.git
685+
686+
687 -- readme.md --
688 # Project\nfoo
689 -- branch_list.1.txt --
690@@ -102,6 +123,7 @@ Description: description
691 Private: true
692 Hidden: true
693 Mirror: false
694+Owner: admin
695 Default Branch: master
696 Branches:
697 - master
698diff --git a/testscript/testdata/repo-import.txtar b/testscript/testdata/repo-import.txtar
699index 4d73a5a43ef7d34cc42f0a85fdba2a44a313753e..d85cdf3790187d2144f21087b39db2e07e9453dc 100644
700--- a/testscript/testdata/repo-import.txtar
701+++ b/testscript/testdata/repo-import.txtar
702@@ -25,6 +25,7 @@ Description: descriptive
703 Private: false
704 Hidden: false
705 Mirror: false
706+Owner: admin
707 Default Branch: main
708 Branches:
709 - main
710diff --git a/testscript/testdata/repo-perms.txtar b/testscript/testdata/repo-perms.txtar
711index bb3e423ac0be09389d08e2efdebe17e066646cf1..cf21151b55dde6e58e7853172aaad422bbb9e775 100644
712--- a/testscript/testdata/repo-perms.txtar
713+++ b/testscript/testdata/repo-perms.txtar
714@@ -82,6 +82,7 @@ Description: desc
715 Private: true
716 Hidden: false
717 Mirror: false
718+Owner: admin
719 Default Branch: master
720 Branches:
721 - master