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 ¶
- Variables
- func AcceptTransfer(ctx context.Context, deps Deps, actorUserID, transferID int64) error
- func Archive(ctx context.Context, deps Deps, actorUserID, repoID int64) error
- func CancelTransfer(ctx context.Context, deps Deps, actorUserID, transferID int64) error
- func DeclineTransfer(ctx context.Context, deps Deps, actorUserID, transferID int64) error
- func ExpirePending(ctx context.Context, deps Deps) (int64, error)
- func HardDelete(ctx context.Context, deps Deps, actorUserID, repoID int64) error
- func Rename(ctx context.Context, deps Deps, p RenameParams) error
- func RequestTransfer(ctx context.Context, deps Deps, p TransferRequestParams) (int64, error)
- func Restore(ctx context.Context, deps Deps, actorUserID, repoID int64) error
- func SetVisibility(ctx context.Context, deps Deps, actorUserID, repoID int64, ...) error
- func SoftDelete(ctx context.Context, deps Deps, actorUserID, repoID int64) error
- func Unarchive(ctx context.Context, deps Deps, actorUserID, repoID int64) error
- type Deps
- type RenameParams
- type TransferRequestParams
Constants ¶
This section is empty.
Variables ¶
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.
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 ¶
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 ¶
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 ¶
CancelTransfer is the sender pulling the offer. Repo-admin actors (owner) can cancel.
func DeclineTransfer ¶
DeclineTransfer flips the row to declined. Recipient is the actor.
func ExpirePending ¶
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 ¶
HardDelete is the worker-driven cascade that runs after the soft- delete grace expires. The order is explicit and auditable:
- Anonymize forks-of-this-repo (clear fork_of_repo_id on children).
- 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).
- DELETE FROM repos — FK cascades handle push_events, webhook_events_pending, repo_collaborators, transfer requests, and any redirect rows pointing at this id.
- RemoveAll the bare repo on disk.
- 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 ¶
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 ¶
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 ¶
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.
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.