| 1 | // SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | |
| 3 | package lifecycle |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "errors" |
| 8 | "fmt" |
| 9 | "strings" |
| 10 | |
| 11 | "github.com/jackc/pgx/v5/pgconn" |
| 12 | "github.com/jackc/pgx/v5/pgtype" |
| 13 | |
| 14 | orgsdb "github.com/tenseleyFlow/shithub/internal/orgs/sqlc" |
| 15 | reposdb "github.com/tenseleyFlow/shithub/internal/repos/sqlc" |
| 16 | usersdb "github.com/tenseleyFlow/shithub/internal/users/sqlc" |
| 17 | ) |
| 18 | |
| 19 | type repoDiskPaths struct { |
| 20 | canonical string |
| 21 | deleted string |
| 22 | } |
| 23 | |
| 24 | func lockRepoName(ctx context.Context, q *reposdb.Queries, db reposdb.DBTX, repo reposdb.Repo) error { |
| 25 | key, err := repoNameLockKey(repo.OwnerUserID, repo.OwnerOrgID, repo.Name) |
| 26 | if err != nil { |
| 27 | return err |
| 28 | } |
| 29 | if err := q.LockRepoOwnerName(ctx, db, key); err != nil { |
| 30 | return fmt.Errorf("lock repo owner/name: %w", err) |
| 31 | } |
| 32 | return nil |
| 33 | } |
| 34 | |
| 35 | func repoNameLockKey(ownerUserID, ownerOrgID pgtype.Int8, name string) (string, error) { |
| 36 | name = strings.ToLower(name) |
| 37 | switch { |
| 38 | case ownerUserID.Valid && !ownerOrgID.Valid: |
| 39 | return fmt.Sprintf("repo-name:user:%d:%s", ownerUserID.Int64, name), nil |
| 40 | case ownerOrgID.Valid && !ownerUserID.Valid: |
| 41 | return fmt.Sprintf("repo-name:org:%d:%s", ownerOrgID.Int64, name), nil |
| 42 | default: |
| 43 | return "", errors.New("lifecycle: repo owner is not xor") |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | func diskPathsForRepo(ctx context.Context, deps Deps, repo reposdb.Repo) (repoDiskPaths, error) { |
| 48 | owner, err := ownerSlugForRepo(ctx, deps, repo) |
| 49 | if err != nil { |
| 50 | return repoDiskPaths{}, err |
| 51 | } |
| 52 | canonical, err := deps.RepoFS.RepoPath(owner, repo.Name) |
| 53 | if err != nil { |
| 54 | return repoDiskPaths{}, fmt.Errorf("canonical repo path: %w", err) |
| 55 | } |
| 56 | deleted, err := deps.RepoFS.DeletedRepoPath(owner, repo.Name, repo.ID) |
| 57 | if err != nil { |
| 58 | return repoDiskPaths{}, fmt.Errorf("deleted repo path: %w", err) |
| 59 | } |
| 60 | return repoDiskPaths{canonical: canonical, deleted: deleted}, nil |
| 61 | } |
| 62 | |
| 63 | func ownerSlugForRepo(ctx context.Context, deps Deps, repo reposdb.Repo) (string, error) { |
| 64 | switch { |
| 65 | case repo.OwnerUserID.Valid && !repo.OwnerOrgID.Valid: |
| 66 | user, err := usersdb.New().GetUserIncludingDeleted(ctx, deps.Pool, repo.OwnerUserID.Int64) |
| 67 | if err != nil { |
| 68 | return "", fmt.Errorf("load repo owner user: %w", err) |
| 69 | } |
| 70 | return user.Username, nil |
| 71 | case repo.OwnerOrgID.Valid && !repo.OwnerUserID.Valid: |
| 72 | org, err := orgsdb.New().GetOrgByID(ctx, deps.Pool, repo.OwnerOrgID.Int64) |
| 73 | if err != nil { |
| 74 | return "", fmt.Errorf("load repo owner org: %w", err) |
| 75 | } |
| 76 | return string(org.Slug), nil |
| 77 | default: |
| 78 | return "", errors.New("lifecycle: repo owner is not xor") |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | func activeRepoNameExists(ctx context.Context, q *reposdb.Queries, db reposdb.DBTX, repo reposdb.Repo) (bool, error) { |
| 83 | switch { |
| 84 | case repo.OwnerUserID.Valid && !repo.OwnerOrgID.Valid: |
| 85 | return q.ExistsRepoForOwnerUser(ctx, db, reposdb.ExistsRepoForOwnerUserParams{ |
| 86 | OwnerUserID: pgtype.Int8{Int64: repo.OwnerUserID.Int64, Valid: true}, |
| 87 | Name: repo.Name, |
| 88 | }) |
| 89 | case repo.OwnerOrgID.Valid && !repo.OwnerUserID.Valid: |
| 90 | return q.ExistsRepoForOwnerOrg(ctx, db, reposdb.ExistsRepoForOwnerOrgParams{ |
| 91 | OwnerOrgID: pgtype.Int8{Int64: repo.OwnerOrgID.Int64, Valid: true}, |
| 92 | Name: repo.Name, |
| 93 | }) |
| 94 | default: |
| 95 | return false, errors.New("lifecycle: repo owner is not xor") |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | func isUniqueViolation(err error) bool { |
| 100 | var pgErr *pgconn.PgError |
| 101 | return errors.As(err, &pgErr) && pgErr.Code == "23505" |
| 102 | } |
| 103 |