lifecycle

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 10, 2026 License: AGPL-3.0 Imports: 16 Imported by: 0

Documentation

Overview

Package lifecycle owns the post-creation mutations of a repo: rename, transfer, archive/unarchive, visibility flip, soft-delete, restore, and hard-delete. Every operation is a single function with strict ordering of DB and FS work; recovery semantics are documented at each call site.

The package is policy-agnostic — callers (web handlers, the hard-delete worker) are expected to have already passed the request through policy.Can with the appropriate Action. Lifecycle's job is to *execute* the change correctly, not decide who's allowed.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidName       = errors.New("lifecycle: invalid name")
	ErrReservedName      = errors.New("lifecycle: name is reserved")
	ErrNameTaken         = errors.New("lifecycle: name already taken on owner")
	ErrRenameRateLimited = errors.New("lifecycle: rename rate limit exceeded")
	ErrSameName          = errors.New("lifecycle: new name equals current")
	ErrTransferToSelf    = errors.New("lifecycle: transfer recipient is the same owner")
	ErrTransferTerminal  = errors.New("lifecycle: transfer no longer pending")
	ErrTransferExpired   = errors.New("lifecycle: transfer expired")
	ErrAlreadyArchived   = errors.New("lifecycle: repo is already archived")
	ErrNotArchived       = errors.New("lifecycle: repo is not archived")
	ErrAlreadyDeleted    = errors.New("lifecycle: repo is already soft-deleted")
	ErrNotDeleted        = errors.New("lifecycle: repo is not soft-deleted")
	ErrPastGrace         = errors.New("lifecycle: soft-delete grace expired; restore unavailable")
)

Errors surfaced by lifecycle ops. Handlers map these to HTTP status codes and friendly user messages.

View Source
var ErrInvalidVisibility = errors.New("lifecycle: visibility must be public or private")

ErrInvalidVisibility is returned when a caller passes a value that isn't "public" or "private". Handlers should map to a 400.

Functions

func AcceptTransfer

func AcceptTransfer(ctx context.Context, deps Deps, actorUserID, transferID int64) error

AcceptTransfer flips the transfer to accepted, updates the repo's owner, writes a redirect row from the old owner+name, and renames the bare repo on disk if the recipient also has a different name in mind (rare; defaults to keeping the same name).

The recipient is the actor in this op. We re-check status under the row lock so concurrent accept/cancel/decline races are settled deterministically by the DB.

func Archive

func Archive(ctx context.Context, deps Deps, actorUserID, repoID int64) error

Archive sets is_archived=true on the repo. Idempotent in spirit — a double-archive returns ErrAlreadyArchived so handlers can surface a "this repo is already archived" friendly message without having to inspect state up front.

func CancelTransfer

func CancelTransfer(ctx context.Context, deps Deps, actorUserID, transferID int64) error

CancelTransfer is the sender pulling the offer. Repo-admin actors (owner) can cancel.

func DeclineTransfer

func DeclineTransfer(ctx context.Context, deps Deps, actorUserID, transferID int64) error

DeclineTransfer flips the row to declined. Recipient is the actor.

func ExpirePending

func ExpirePending(ctx context.Context, deps Deps) (int64, error)

ExpirePending sweeps every pending transfer past its expires_at. Returns the count of rows flipped — the worker uses this for observability. Audit logs are emitted in bulk via a single row that records the count; per-transfer audit lines would be noisy and the individual transfer rows themselves carry the timestamp.

func HardDelete

func HardDelete(ctx context.Context, deps Deps, actorUserID, repoID int64) error

HardDelete is the worker-driven cascade that runs after the soft- delete grace expires. The order is explicit and auditable:

  1. Anonymize forks-of-this-repo (clear fork_of_repo_id on children).
  2. Drop the redirect rows that point at this repo (other repos keeping us as their old name; keep redirects pointed away from us so old URLs of *this* repo's previous identity are still resolvable from the now-deleted row, until that repo is also deleted by ON DELETE CASCADE on the FK).
  3. DELETE FROM repos — FK cascades handle push_events, webhook_events_pending, repo_collaborators, transfer requests, and any redirect rows pointing at this id.
  4. RemoveAll the bare repo on disk.
  5. Audit-log the hard-delete with a snapshot of the removed row in the meta payload so the audit row is self-contained even after the repo_id is gone.

The op refuses to run on a repo that isn't past its soft-delete grace window — defense-in-depth even though the worker query already gates this. ActorUserID is typically the worker's instance id encoded in some way; pass 0 to record "system" in audit.

func Rename

func Rename(ctx context.Context, deps Deps, p RenameParams) error

Rename performs a same-owner rename: validate → tx (insert redirect, update repos.name) → FS Move → audit. On FS failure the DB tx is reverted via a compensating UPDATE so the persistent state is consistent. The caller is responsible for policy.Can; we don't re-check here.

func RequestTransfer

func RequestTransfer(ctx context.Context, deps Deps, p TransferRequestParams) (int64, error)

RequestTransfer creates a pending transfer with a 7-day TTL. Returns the new request id. Confirmation typing ("type owner/repo to confirm") belongs in the handler — this function trusts its inputs.

func Restore

func Restore(ctx context.Context, deps Deps, actorUserID, repoID int64) error

Restore clears repos.deleted_at within the grace window. Past the window the row may still exist (worker hasn't run yet) but we refuse — the operator-visible contract is "7 days, then it's gone".

func SetVisibility

func SetVisibility(ctx context.Context, deps Deps, actorUserID, repoID int64, newVisibility string) error

SetVisibility flips a repo between public and private. Forks remain independent (separate repos at the data layer), and existing clones already pulled remain — that's git's nature, not a bug. The UI is responsible for surfacing the "future clones will be private" caveat to the user.

func SoftDelete

func SoftDelete(ctx context.Context, deps Deps, actorUserID, repoID int64) error

SoftDelete sets repos.deleted_at to now. The repo disappears from listings and the home page returns 404 for non-owners (auth-aware). The bare repo on disk is left alone until the hard-delete worker runs at the end of the grace window.

func Unarchive

func Unarchive(ctx context.Context, deps Deps, actorUserID, repoID int64) error

Unarchive clears is_archived. Same idempotency contract as Archive.

Types

type Deps

type Deps struct {
	Pool   *pgxpool.Pool
	RepoFS *storage.RepoFS
	Audit  *audit.Recorder
	Logger *slog.Logger
	Now    func() time.Time
}

Deps wires the orchestrator. Construct once and pass to every Op. All Ops are stateless w.r.t. Deps so a single value can serve many concurrent requests.

type RenameParams

type RenameParams struct {
	ActorUserID int64 // who is initiating; recorded in audit
	RepoID      int64
	OwnerUserID int64  // current owner — also gives us the FS path
	OwnerName   string // current owner username (for FS path)
	OldName     string // current repo name
	NewName     string // requested name
}

RenameParams is what a same-owner rename takes.

type TransferRequestParams

type TransferRequestParams struct {
	ActorUserID     int64
	RepoID          int64
	FromUserID      int64
	ToPrincipalKind string // "user" — "org" arrives in S31
	ToPrincipalID   int64
}

TransferRequestParams describes a new transfer offer.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL