// Package transport writes chapters 11 and 18 once, because a rule on one transport is not a rule. package transport import ( "context" "database/sql" "errors" "fmt" "os" "time" "path/filepath" "github.com/barerepo/server/internal/config" "github.com/barerepo/server/internal/gitx" "github.com/barerepo/server/internal/repo" "github.com/barerepo/server/internal/repocfg" "github.com/barerepo/server/internal/store" ) type Server struct { Cfg config.Config DB *store.DB // Bin is the barerepo binary the hooks call. Empty means this executable. Bin string } var ( // A reader without access gets this too, or "forbidden" would announce the private repository. ErrNotFound = errors.New("no such repository") ErrDenied = errors.New("denied") ) // Redirect is a moved repository, kept forever, because a 404 breaks every clone. 44.2. type Redirect struct{ Owner, Name string } func (r Redirect) Error() string { return "moved to " + r.Owner + "/" + r.Name } // Result is a resolved repository. type Result struct { Dir string Owner string Name string Config repocfg.Config Created bool // this call created it, per chapter 11 // Warning reaches the pusher's terminal, because a malformed config must not lock anyone out. Warning string } // Intent exists because git over http splits a push in two, and only the second is a push. type Intent int const ( // Read is git-upload-pack: clone, fetch, ls-remote. Read Intent = iota // Announce must never create, or ls-remote and push --dry-run would claim names by accident. Announce // Write is the push itself, and is the only thing that may create. Write ) func (i Intent) write() bool { return i != Read } // Open resolves a repository for account user, who is already authenticated. func (s *Server) Open(ctx context.Context, owner, name, user string, intent Intent) (*Result, error) { write := intent.write() if to, err := s.redirect(ctx, owner, name); err != nil { return nil, err } else if to != nil { return nil, *to } if !repo.Exists(s.Cfg.Paths.Repos, owner, name) { switch intent { case Read: return nil, ErrNotFound case Announce: // Answer "no refs" instead of creating, and let the push that follows create it. if err := s.mayCreate(ctx, owner, name, user); err != nil { return nil, err } dir, err := s.emptyTemplate(ctx) if err != nil { return nil, err } return &Result{Dir: dir, Owner: owner, Name: name, Config: repocfg.Default()}, nil } return s.create(ctx, owner, name, user) } dir, err := repo.Dir(s.Cfg.Paths.Repos, owner, name) if err != nil { return nil, ErrNotFound } res := &Result{Dir: dir, Owner: owner, Name: name} res.Config, err = repocfg.Load(ctx, dir) if err != nil { // Chapter 14: fall back and warn, so the file cannot lock the owner out of fixing it. res.Warning = fmt.Sprintf("%s does not parse: %v", repocfg.Path, err) if res.Config.FellBackTo != "" { res.Warning += fmt.Sprintf("\nthe settings from %.7s are still in force", res.Config.FellBackTo) } else { res.Warning += "\nno earlier version parses either, so the defaults are in force" } } if !res.Config.MayRead(owner, user) { return nil, ErrNotFound } if write && res.Config.Repo.Archived && user != owner { return nil, fmt.Errorf("%w: this repository is archived. the owner can unarchive it in %s", ErrDenied, repocfg.Path) } // pre-receive decides which refs may be written, so reaching receive-pack grants nothing. return res, nil } // mayCreate answers whether this push could create the repository, without creating it. func (s *Server) mayCreate(ctx context.Context, owner, name, user string) error { // Someone else's namespace answers the same whether the name is free or private, or the two messages list what alice keeps private. Chapter 18. switch { case user == "", user != owner: return ErrNotFound case !s.Cfg.Behavior.AllowPushToCreate: return fmt.Errorf("%w: this server does not create repositories by push. make it at %s/new first", ErrDenied, s.Cfg.Server.ExternalURL) } if _, err := s.DB.Account(ctx, owner); err != nil { return ErrNotFound } if why := s.Claimed(ctx, owner, name); why != "" { return fmt.Errorf("%w: %s", ErrDenied, why) } return nil } // Claimed says why a name cannot be taken, or nothing when it can. Chapters 21.2 and 44.4. func (s *Server) Claimed(ctx context.Context, owner, name string) string { // A renamed name is never freed, or the redirect points at a repository that is not the one. 21.2. if to, err := s.redirect(ctx, owner, name); err == nil && to != nil { return owner + "/" + name + " is now " + to.Owner + "/" + to.Name + ". the old name is kept forever, so nothing that points at it breaks." } // A deleted name is held while its data waits out the window, or a restore has nowhere to land. if repo.InTrash(s.Cfg.Paths.Repos, owner, name) { return owner + "/" + name + " was deleted. its name is held for 30 days, then it is free." } return "" } // emptyTemplate is one refless repository shared by every advertisement, so git states its own caps. func (s *Server) emptyTemplate(ctx context.Context) (string, error) { dir := filepath.Join(s.Cfg.Paths.Cache, "empty.git") if _, err := os.Stat(filepath.Join(dir, "HEAD")); err == nil { return dir, nil } if err := os.MkdirAll(s.Cfg.Paths.Cache, 0o750); err != nil { return "", err } if _, err := gitx.Run(ctx, "", "init", "--bare", "--initial-branch", "master", "--", dir); err != nil { return "", err } return dir, nil } // create implements push-to-create, chapter 11 and appendix D. func (s *Server) create(ctx context.Context, owner, name, user string) (*Result, error) { if err := s.mayCreate(ctx, owner, name, user); err != nil { return nil, err } // HEAD is a placeholder until post-receive sees which branch actually arrived. dir, err := repo.Create(ctx, s.Cfg.Paths.Repos, owner, name, "master", s.bin()) if err != nil { return nil, err } if _, err := s.DB.ExecContext(ctx, `INSERT INTO repos (owner, name, created_at) VALUES (?, ?, ?)`, owner, name, time.Now().Unix()); err != nil { os.RemoveAll(dir) return nil, err } cfg := repocfg.Default() // Chapter 11: publishing by accident is final, and hiding by accident is one config line. cfg.Repo.Visibility = "private" return &Result{Dir: dir, Owner: owner, Name: name, Config: cfg, Created: true}, nil } // redirect follows a rename or transfer. Chapter 44.2. func (s *Server) redirect(ctx context.Context, owner, name string) (*Redirect, error) { var to Redirect err := s.DB.QueryRowContext(ctx, `SELECT new_owner, new_name FROM redirects WHERE old_owner = ? AND old_name = ?`, owner, name).Scan(&to.Owner, &to.Name) if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { return nil, err } return &to, nil } func (s *Server) bin() string { if s.Bin != "" { return s.Bin } if p, err := os.Executable(); err == nil { return p } return "barerepo" }