ikurotime / gitgud

public
Add repo service with create, list and visibility checks
DDavid committed on 2026-06-28 00:57 commit b4ed1e6
internal/app/repo_service.go +70 −0
+package app
+
+import (
+ "context"
+ "fmt"
+ "regexp"
+ "strings"
+
+ "gitgud/internal/domain"
+)
+
+var repoNameRe = regexp.MustCompile(`^[a-z0-9][a-z0-9-_.]{0,99}$`)
+
+type RepoService struct {
+ repos domain.RepositoryRepository
+ git domain.GitService
+}
+
+func NewRepoService(repos domain.RepositoryRepository, git domain.GitService) *RepoService {
+ return &RepoService{repos: repos, git: git}
+}
+
+func (s *RepoService) CreateRepo(ctx context.Context, owner *domain.User, name, description string, private bool) (*domain.Repository, error) {
+ name = strings.ToLower(strings.TrimSpace(name))
+ if name == "." || name == ".." || !repoNameRe.MatchString(name) {
+ return nil, fmt.Errorf("name must be 1-100 chars of a-z, 0-9, -, _ or . and start with a letter or number: %w", domain.ErrValidation)
+ }
+
+ repo := &domain.Repository{
+ OwnerID: owner.ID,
+ OwnerName: owner.Username,
+ Name: name,
+ Description: strings.TrimSpace(description),
+ IsPrivate: private,
+ DefaultBranch: "main",
+ }
+
+ if err := s.repos.Create(ctx, repo); err != nil {
+ return nil, err
+ }
+
+ if err := s.git.InitBare(ctx, owner.Username, name, repo.DefaultBranch); err != nil {
+ _ = s.repos.Delete(ctx, repo.ID)
+ return nil, fmt.Errorf("init repository on disk: %w", err)
+ }
+
+ return repo, nil
+}
+
+func (s *RepoService) Get(ctx context.Context, owner, name string) (*domain.Repository, error) {
+ return s.repos.ByOwnerAndName(ctx, owner, name)
+}
+
+func (s *RepoService) ListByOwner(ctx context.Context, ownerID int64) ([]*domain.Repository, error) {
+ return s.repos.ListByOwner(ctx, ownerID)
+}
+
+func CanView(repo *domain.Repository, viewer *domain.User) error {
+ if !repo.IsPrivate {
+ return nil
+ }
+ if viewer != nil && viewer.ID == repo.OwnerID {
+ return nil
+ }
+ return domain.ErrNotFound
+}
+
+func CanPush(repo *domain.Repository, viewer *domain.User) bool {
+ return viewer != nil && viewer.ID == repo.OwnerID
+}
internal/app/user_service.go +4 −0
package app
import (
"context"
"errors"
"fmt"
"regexp"
"strings"
"gitgud/internal/domain"
)
var usernameRe = regexp.MustCompile(`^[a-z0-9-]{1,39}$`)
type UserService struct {
users domain.UserRepository
hasher domain.PasswordHasher
}
func NewUserService(u domain.UserRepository, h domain.PasswordHasher) *UserService {
return &UserService{users: u, hasher: h}
}
func (s *UserService) Register(ctx context.Context, username, email, password string) (*domain.User, error) {
username = strings.ToLower(strings.TrimSpace(username))
email = strings.TrimSpace(email)
if username == "" || email == "" || password == "" {
return nil, fmt.Errorf("username, email and password are required: %w", domain.ErrValidation)
}
if !usernameRe.MatchString(username) {
return nil, fmt.Errorf("username must be 1-39 chars of a-z, 0-9 or -: %w", domain.ErrValidation)
}
if len(password) < 8 {
return nil, fmt.Errorf("password must be at least 8 characters: %w", domain.ErrValidation)
}
if _, err := s.users.ByUsername(ctx, username); err == nil {
return nil, fmt.Errorf("username %q is taken: %w", username, domain.ErrConflict)
} else if !errors.Is(err, domain.ErrNotFound) {
return nil, err
}
hash, err := s.hasher.Hash(password)
if err != nil {
return nil, fmt.Errorf("hash password: %w", err)
}
u := &domain.User{
Username: username,
Email: email,
PasswordHash: hash,
}
if err := s.users.Create(ctx, u); err != nil {
return nil, err
}
return u, nil
}
func (s *UserService) Authenticate(ctx context.Context, username, password string) (*domain.User, error) {
u, err := s.users.ByUsername(ctx, strings.ToLower(strings.TrimSpace(username)))
if err != nil {
if errors.Is(err, domain.ErrNotFound) {
return nil, domain.ErrUnauthorized
}
return nil, err
}
if err := s.hasher.Compare(u.PasswordHash, password); err != nil {
return nil, domain.ErrUnauthorized
}
return u, nil
}
func (s *UserService) ByID(ctx context.Context, id string) (*domain.User, error) {
return s.users.ByID(ctx, id)
}
+
+func (s *UserService) ByUsername(ctx context.Context, name string) (*domain.User, error) {
+ return s.users.ByUsername(ctx, strings.ToLower(strings.TrimSpace(name)))
+}