go.bigb.es/auxilia

v0.3.0
Doc Versions Source

Documentation

Package sync provides legatus steps for keeping a local file tree and a remote host in agreement: Push (upload changed files), Pull (download with per-file conflict resolution), Diff (read-only unified diff), and a git-drift battery (EnsureRepo, IsDirty, Commit, Status, plus a GitGuard gate step).

All remote work goes through legatus.Conn. Every interactive decision goes through the FlowCtx prompter (PC2); the non-interactive default declines, so destructive paths (delete, conflict overwrite) skip rather than force.

Index

Variables

var ErrEnvAborted = culpa.WithCode(culpa.New("sync: .env sync aborted by operator"), "SYNC_ENV_ABORTED")

ErrEnvAborted is raised when the operator chooses "abort" on a .env diff, so the deploy stops before any later step (e.g. post_push).

var ErrRemoteDirty = culpa.WithCode(
	culpa.New("sync: remote git repo has uncommitted changes"),
	"SYNC_REMOTE_DIRTY",
)

ErrRemoteDirty is the sentinel raised by GitGuard when the remote repo has uncommitted changes that would be clobbered by a push.

Functions

f func Commit

src
func Commit(ctx context.Context, conn legatus.Conn, remoteRoot, message string) error

Commit stages all changes and commits them with message. It is a no-op when there is nothing to commit.

f func CommitOrRollback

src
func CommitOrRollback(ctx context.Context, conn legatus.Conn, root, okMsg, failMsg string, apply func() error) error

CommitOrRollback runs apply(); on success it commits okMsg, on failure it commits failMsg and returns the original apply error — so the remote repo is left committed (never dirty) regardless of how apply fails, and the next push is not blocked by uncommitted drift. A failure of the rollback commit itself is wrapped onto the apply error, never swallowed (GPC6).

f func EnsureGitignore

src
func EnsureGitignore(ctx context.Context, conn legatus.Conn, remoteRoot string, entries []string) (bool, error)

EnsureGitignore writes (idempotently) a .gitignore at remoteRoot containing the given entries, deduped and sorted, inside the managed marker block. Manual lines outside the block survive. It performs no write when the managed block already matches, reporting changed=false.

f func EnsureRepo

src
func EnsureRepo(ctx context.Context, conn legatus.Conn, remoteRoot string) error

EnsureRepo initializes a git repository at remoteRoot if one does not already exist (idempotent). A missing directory or a git failure raises.

f func IsDirty

src
func IsDirty(ctx context.Context, conn legatus.Conn, remoteRoot string) (bool, string, error)

IsDirty reports whether the remote repo has uncommitted changes, returning the porcelain status. A non-zero git exit (e.g. not a repository) raises rather than being swallowed as "clean".

f func Status

src
func Status(ctx context.Context, conn legatus.Conn, remoteRoot string) (string, error)

Status returns a short human-readable summary of the remote repo state: "no repo", "clean", or "dirty (<n> changes)".

Types

T type Diff

src
type Diff struct {
	// Files are individual paths relative to LocalRoot/RemoteRoot.
	Files []string
	// Dirs are directory paths (relative) compared recursively.
	Dirs []string
	// LocalRoot is the base of the local tree.
	LocalRoot string
	// RemoteRoot is the base of the remote tree.
	RemoteRoot string
	// Normalize, when set, is applied to both local and remote bytes before the
	// equality test, so a JSON-canonicalizer or whitespace-normalizer makes the
	// comparison semantic. The rendered diff is of the normalized forms.
	Normalize func([]byte) ([]byte, error)
	// Summary, when set, makes Apply report a content-free per-file verdict
	// ("rel: same" / "rel: differs") instead of a unified diff — so secret-bearing
	// files can be diffed for change-detection without printing their content.
	Summary bool
}

Diff is a read-only step: it downloads each remote file, compares it to its local counterpart, and reports a unified diff. It never writes anywhere — Outcome.Changed is always false (PC: idempotent, observation-only). The rendered diff is in Outcome.Data (string); empty string means no differences.

m func (*Diff) Apply

src
func (d *Diff) Apply(fc *legatus.FlowCtx) (legatus.Outcome, error)

Apply renders the comparison and reports it. It is read-only: Changed is false. In Summary mode the Data is a content-free per-file verdict; otherwise it is the concatenated unified diffs of the differing files.

m func (*Diff) Check

src
func (d *Diff) Check(fc *legatus.FlowCtx) (bool, error)

Check reports whether any file differs.

m func (*Diff) Name

src
func (d *Diff) Name() string

Name identifies the step.

T type EnvSync

src
type EnvSync struct {
	// File is the file name relative to the roots; defaults to ".env".
	File string
	// LocalRoot is the base of the local tree.
	LocalRoot string
	// RemoteRoot is the base of the remote tree.
	RemoteRoot string
	// Sudo stages the upload with sudo (for a root-owned remote .env).
	Sudo bool
	// Owner, if set, chowns the uploaded file (user or user:group).
	Owner string
}

EnvSync syncs one sensitive, untracked file (default ".env") that the normal Push skips. On a diff it shows the change and asks skip|overwrite|abort; abort halts the flow. When only the local file exists it asks to upload. The file's content reaches only the operator-facing sink (fc.Out()) and the prompter, never the structured logger (it is a secret).

m func (*EnvSync) Apply

src
func (e *EnvSync) Apply(fc *legatus.FlowCtx) (legatus.Outcome, error)

Apply syncs the file through the prompter. abort returns ErrEnvAborted.

m func (*EnvSync) Check

src
func (e *EnvSync) Check(fc *legatus.FlowCtx) (bool, error)

Check reports whether the local file exists and the remote is absent or differs.

m func (*EnvSync) Name

src
func (e *EnvSync) Name() string

Name identifies the step.

T type GitGuard

src
type GitGuard struct {
	// RemoteRoot is the remote repository directory.
	RemoteRoot string
}

GitGuard is a gate step: it blocks the flow when the remote git repo is dirty, preventing a push from clobbering uncommitted remote drift. Check reports need=true when dirty; Apply raises ErrRemoteDirty.

m func (*GitGuard) Apply

src
func (g *GitGuard) Apply(fc *legatus.FlowCtx) (legatus.Outcome, error)

Apply raises if the remote is dirty; otherwise reports a clean, no-change outcome.

m func (*GitGuard) Check

src
func (g *GitGuard) Check(fc *legatus.FlowCtx) (bool, error)

Check reports whether the remote is dirty (needs attention).

m func (*GitGuard) Name

src
func (g *GitGuard) Name() string

Name identifies the step.

T type Gitignore

src
type Gitignore struct {
	// RemoteRoot is the remote repository directory holding the .gitignore.
	RemoteRoot string
	// Entries are the patterns written inside the managed block.
	Entries []string
}

Gitignore is the step form of EnsureGitignore, so a deploy Flow can ensure the managed .gitignore declaratively alongside GitGuard.

m func (*Gitignore) Apply

src
func (g *Gitignore) Apply(fc *legatus.FlowCtx) (legatus.Outcome, error)

Apply writes the managed block when it differs, delegating to EnsureGitignore.

m func (*Gitignore) Check

src
func (g *Gitignore) Check(fc *legatus.FlowCtx) (bool, error)

Check reports whether the managed block would change.

m func (*Gitignore) Name

src
func (g *Gitignore) Name() string

Name identifies the step.

T type Pull

src
type Pull struct {
	// Files are individual paths relative to LocalRoot/RemoteRoot.
	Files []string
	// Dirs are directory paths (relative) pulled recursively.
	Dirs []string
	// LocalRoot is the base of the local tree.
	LocalRoot string
	// RemoteRoot is the base of the remote tree.
	RemoteRoot string
	// Mode selects conflict resolution; the zero value is PullConfirm.
	Mode PullMode
}

Pull downloads remote files into the local tree. A new file (no local counterpart) is written directly. A conflict (local exists and differs) is resolved through the prompter (PC2): the non-interactive default declines, so the conflict is skipped and the local file is left untouched — never silently overwritten.

m func (*Pull) Apply

src
func (p *Pull) Apply(fc *legatus.FlowCtx) (legatus.Outcome, error)

Apply downloads each remote file and applies it. New files are written; conflicts go through the prompter. Outcome.Data is the sorted list of applied relative paths.

m func (*Pull) Check

src
func (p *Pull) Check(fc *legatus.FlowCtx) (bool, error)

Check reports whether any remote file is new or differs locally.

m func (*Pull) Name

src
func (p *Pull) Name() string

Name identifies the step.

T type PullMode

src
type PullMode int

PullMode selects how Pull resolves a conflict (local exists and differs).

const (
	// PullConfirm asks a yes/no Confirm to overwrite the whole file (default).
	PullConfirm PullMode = iota
	// PullInteractive offers per-file apply|skip|patch|quit and, in patch mode, a
	// per-hunk picker.
	PullInteractive
)

T type Push

src
type Push struct {
	// Files are individual paths relative to LocalRoot/RemoteRoot.
	Files []string
	// Dirs are directory paths (relative) synced recursively.
	Dirs []string
	// LocalRoot is the base of the local tree.
	LocalRoot string
	// RemoteRoot is the base of the remote tree.
	RemoteRoot string
	// Delete removes remote-only files under Dirs (gated by the prompter).
	Delete bool
}

Push uploads local files to the remote, comparing by content hash so unchanged files are skipped (PC4: Check reports need=false when every file already matches). When Delete is set, remote-only files under the synced Dirs are removed — but only after the prompter confirms (PC2); the non-interactive default declines, so a stale file is kept, never force-deleted.

m func (*Push) Apply

src
func (p *Push) Apply(fc *legatus.FlowCtx) (legatus.Outcome, error)

Apply uploads changed files and, when Delete is set, removes confirmed remote-only files. Outcome.Data is the sorted list of uploaded relative paths. Dry-run is handled by the engine (IV4): Apply only runs for real.

m func (*Push) Check

src
func (p *Push) Check(fc *legatus.FlowCtx) (bool, error)

Check reports whether any file needs uploading. need=false when all match.

m func (*Push) Name

src
func (p *Push) Name() string

Name identifies the step.