Documentation
¶
Overview ¶
Package gigit is a lightweight, embeddable, git-like revision control system. No branches, no merges; history is strictly linear and every revision is a complete snapshot of the tracked directory.
The system is two self-contained subsystems and a thin top level. The subsystems share exactly one piece of vocabulary: (path, frev) — a tracked path and its per-file version counter.
- The indexing subsystem (gigit/index) knows what the directory is, per revision: paths, frevs, status, and a 7-char short sha per change.
- The blob subsystem (gigit/blobs) knows bytes and shas per (path, frev). It never sees revisions of the directory.
Every change in the system is a changeset (index) and a patch (blobs). The pair is the complete representation of a revision, locally and on the wire — and blob storage itself is just patches at rest.
This root package holds the shared types and the subsystem interfaces. Implementations live in gigit/index, gigit/blobs, and gigit/remote (the HTTP client); the top-level Repo that binds them lives in gigit/repo, and gigit/server exposes any Remote over HTTP.
See docs/design.md for the full design, including the wire formats and the crash-safety rules.
Index ¶
- Constants
- Variables
- func HashBytes(content []byte) [32]byte
- func ShortSha(sha [32]byte) string
- func ValidatePath(path string) error
- func WriteEntryHeader(w io.Writer, hdr *EntryHeader) error
- func WritePatchHeader(w io.Writer, hdr *PatchHeader) error
- type Blobs
- type Change
- type Changeset
- type ClientConfig
- type ClientState
- type Config
- type Directive
- type Duration
- type EntryHeader
- type FleetClient
- type FleetConfig
- type FrameType
- type Index
- type IndexConfig
- type LogFilter
- type Op
- type Orders
- type PatchHeader
- type Remote
- type Report
- type ReportHandler
- type Reporter
- type UIConfig
- type Version
Constants ¶
const ( EntryHeaderSize = 43 PatchHeaderSize = 8 // PatchFormatVersion is the current patch/blob stream format. PatchFormatVersion = 1 )
const ( // FrevLatest selects a path's newest version; frevs start at 1. FrevLatest uint32 = 0 // PruneAll, as a minimum frev in Blobs.Prune, drops the path's file // entirely. PruneAll uint32 = ^uint32(0) )
const ConfigFile = "config.yml"
ConfigFile is the optional configuration object inside MetaDir. An absent file is the zero Config, which is exactly phase-1 behavior. It lives inside .gg, so it is metadata by definition — never tracked, never synced; each repository's config is its own.
const MetaDir = ".gg"
MetaDir is the metadata directory name at the workdir root. The tracked universe is every file under the workdir except MetaDir — there are no ignore rules in v1: a config directory is wholly owned.
const ShortShaLen = 7
ShortShaLen is the length of the index's sha prefix. Change detection is probabilistic at 28 bits — an accepted trade for a lean index; the knob for strengthening it is a longer prefix, not a cache.
Variables ¶
var ( // ErrNotLinear is returned by Index.Append when a preset Rev does not // equal Max()+1. History is strictly linear; there is no other place // a new revision can go. ErrNotLinear = errors.New("gigit: revision is not max+1, history is linear") // ErrPruned is returned for revisions below the base: Snapshot, Diff, // and Log cannot reach below a shallow repository's base revision. // Its HTTP form is 410 Gone — the destination should re-bootstrap. ErrPruned = errors.New("gigit: revision pruned away, below the base") // ErrUnknownRevision is returned for revisions above max. // Its HTTP form is 404. ErrUnknownRevision = errors.New("gigit: unknown revision, above max") // ErrDirty is returned when an operation would silently lose // uncommitted work: a checkout over any dirt, or a restore targeting a // dirty path. Discarding always requires force. ErrDirty = errors.New("gigit: uncommitted changes in the way, use force to discard") // ErrBehind is returned by Commit when current < max: new revisions // append after max, so update or checkout to max first // ("checkout visits; committing Diff adopts"). ErrBehind = errors.New("gigit: current is behind max, checkout max before committing") // ErrDivergence is returned when two repositories disagree about // history they should share: a patch entry whose (path, frev) exists // locally with a different sha, or a last-shared changeset that // compares unequal during update. ErrDivergence = errors.New("gigit: histories diverge") // ErrUnknownVersion is returned by CreatePatch for a (path, frev) the // blob store does not hold. Its HTTP form is 409 — a divergent index. ErrUnknownVersion = errors.New("gigit: unknown (path, frev)") // ErrVersion is returned by AppendPatch for a patch stream whose // format version is unsupported. ErrVersion = errors.New("gigit: unsupported patch format version") // ErrInvalidPath is returned for paths violating the path rules — // incoming patches are untrusted input and materialization must never // escape the workdir. ErrInvalidPath = errors.New("gigit: invalid path") // ErrCorrupt is returned when stored or received content fails its // sha256 check, or a stream is structurally broken. ErrCorrupt = errors.New("gigit: corrupt data") // ErrUnknownClient is returned by a strict fleet for reports from // undeclared clients. Its HTTP form is 403. ErrUnknownClient = errors.New("gigit: unknown client, fleet is strict") // ErrReportsUnsupported is returned by the remote client when the // server has no /report endpoint (a phase-1 server). Followers detect // it and fall back to plain polling. ErrReportsUnsupported = errors.New("gigit: server does not support reports") )
var PatchMagic = [4]byte{'G', 'G', 'P', 'A'}
PatchMagic opens every patch stream and .blob file.
Functions ¶
func HashBytes ¶
HashBytes is the system's content hash: sha256 over the uncompressed content. The sha is established once, at change detection, and flows down — Save validates it, never re-mints it.
func ValidatePath ¶
ValidatePath enforces the path rules. It is applied at Save and at AppendPatch/checkout alike, because incoming patches are untrusted input and materialization must never escape the workdir:
- Relative, "/"-separated; no leading "/", no "."/".."/empty components.
- No NUL or control characters; no "+" (the blob separator); no "\" (portability).
- Must not be, or begin with, ".gg".
- Encoded filename ≤ 255 bytes. (Case-fold collisions are checked by the blob store against its directory, deterministically on every filesystem.)
func WriteEntryHeader ¶
func WriteEntryHeader(w io.Writer, hdr *EntryHeader) error
WriteEntryHeader writes the normative 43-byte big-endian layout.
func WritePatchHeader ¶
func WritePatchHeader(w io.Writer, hdr *PatchHeader) error
WritePatchHeader writes the normative 8-byte big-endian layout.
Types ¶
type Blobs ¶
type Blobs interface {
// Save appends an entry. The caller supplies the content's sha256 —
// established upstream at change detection — and Save MAY validate it
// (cheap insurance; retrieve-time verification is the real backstop).
// The only thing the blob layer mints is the frev: the path's last
// frev + 1. Compression happens here, exactly once — every later
// movement of this version is a verbatim byte copy.
Save(path string, content []byte, sha [32]byte) (frev uint32, err error)
// Retrieve hop-scans to the entry, decompresses, verifies the full sha.
// The returned Version resolves FrevLatest and lets callers cross-check
// the index's short sha.
Retrieve(path string, frev uint32) ([]byte, Version, error)
// Query hop-scans one path's file (FrevLatest → newest). Rarely needed:
// the index's short shas answer most callers.
Query(path string, frev uint32) (Version, error)
// CreatePatch streams the entries for the given modify changes,
// copied verbatim from the .blob files — no recompression.
CreatePatch(changes []Change) (io.ReadCloser, error)
// AppendPatch validates the PatchHeader (magic, supported version →
// ErrVersion), then routes entries to their .blob files: frev > the
// path's last → append verbatim; frev ≤ last → skip, verifying sha
// equality where that frev exists (ErrDivergence otherwise).
// Idempotent and resumable; entries arrive in increasing frev order
// per path.
AppendPatch(patch io.Reader) error
// Prune drops history: per path, delete entries below the given
// minimum frev (rewrite via temp + rename); a path mapped to
// PruneAll loses its file entirely. Paths absent from the map are
// untouched.
Prune(min map[string]uint32) error
}
Blobs is the blob subsystem: versioned content storage per (path, frev). It never sees revisions of the directory. The entry is the single unit of both storage and exchange — a .blob file and a patch are the same format, and every movement of a saved frame is a verbatim byte copy.
type Change ¶
type Change struct {
Path string `yaml:"path" json:"path"`
Op Op `yaml:"op" json:"op"`
Frev uint32 `yaml:"frev,omitempty" json:"frev,omitempty"` // version identity; modify only
Sha string `yaml:"sha,omitempty" json:"sha,omitempty"` // 7-char sha256 prefix; modify only
}
Change is one line of a changeset: the target state of one path.
Sha is a comparison heuristic and cross-check, deliberately not an identity — Frev is the identity, and short-sha equality across different frevs must never be acted on. The authoritative full sha256 lives in the blob entry header, of which this is a prefix.
func Unify ¶
Unify folds changesets (in rev order) into the bridging changes: per path the last op wins, so a file touched many times appears once, with its newest frev. Pure — needs no storage. The input is sorted by rev defensively; the output is sorted by path.
func (Change) MarshalYAML ¶
MarshalYAML renders a change as a single flow-style mapping, so a changeset reads one change per line:
- { path: conf/app/settings.json, op: modify, frev: 7, sha: a1b2c3d }
type Changeset ¶
type Changeset struct {
Rev uint64 `yaml:"rev" json:"rev"`
Time time.Time `yaml:"time,omitempty" json:"time,omitzero"` // log changesets only
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"` // log changesets only
Changes []Change `yaml:"changes" json:"changes"`
}
Changeset is the single index object: log file, snapshot, or diff result.
Changeset is reserved for historical records (log files and the /changesets wire). Everything derived — snapshots, diff results, unified updates — is a bare []Change: no rev, no time, no comment.
type ClientConfig ¶
type ClientConfig struct {
Name string `yaml:"name,omitempty"` // identity in reports; default: hostname
Origin string `yaml:"origin,omitempty"` // default src for update / pull / follow / recreate
Poll Duration `yaml:"poll,omitempty"` // follow-mode cadence; default 5s
}
ClientConfig describes this repository as a destination.
type ClientState ¶
type ClientState string
ClientState is the client's own verdict on itself, carried in reports.
const ( StateOk ClientState = "ok" // operating, tracking StateStable ClientState = "stable" // at target, clean, for ≥3 consecutive beats StateFailed ClientState = "failed" // last maneuver failed; Detail says why )
type Config ¶
type Config struct {
Index IndexConfig `yaml:"index,omitempty"`
Client ClientConfig `yaml:"client,omitempty"`
Fleet FleetConfig `yaml:"fleet,omitempty"`
UI UIConfig `yaml:"ui,omitempty"`
}
Config is the .gg/config.yml object. Precedence everywhere: explicit code options override the file, CLI flags override both.
func LoadConfig ¶
LoadConfig reads <dir>/.gg/config.yml. A missing file is the zero Config, not an error.
type Directive ¶
type Directive string
Directive is a server-issued recovery maneuver, delivered with orders. Delivery is repeated and idempotent: the client acts on DirectiveRecreate only while its base < Orders.RecreateTo, so a directive raced with its own fulfillment cannot loop.
type Duration ¶
Duration is a time.Duration that reads and writes as "5s"/"2m" in YAML.
func (Duration) MarshalYAML ¶
type EntryHeader ¶
type EntryHeader struct {
Frev uint32 // explicit — NOT ordinal; gaps are legal and normal
Type FrameType // frame encoding
PathLen uint16 // path bytes following the header
Len uint32 // frame bytes following the path
Sha [32]byte // sha256 of the uncompressed content (authoritative)
}
EntryHeader opens every entry, followed by PathLen bytes of path and Len bytes of frame. The on-disk/wire format is *defined* as:
err := binary.Write(buf, binary.BigEndian, &hdr) // exactly 43 bytes
This works because every field is fixed-size (declared order, no padding); adding a variable-size field would silently break it — don't.
func ReadEntryHeader ¶
func ReadEntryHeader(r io.Reader) (EntryHeader, error)
ReadEntryHeader reads the normative 43-byte big-endian layout. io.EOF means a clean end of stream; a partial header is ErrCorrupt.
type FleetClient ¶
type FleetClient struct {
// Target is "max" (default, follow the head) or a decimal revision
// number (pinned: "run this revision").
Target string `yaml:"target,omitempty"`
}
FleetClient declares one expected client.
func (FleetClient) ResolveTarget ¶
func (this FleetClient) ResolveTarget(max uint64) (uint64, error)
ResolveTarget resolves the declared target against the source's max.
type FleetConfig ¶
type FleetConfig struct {
GraceWarn Duration `yaml:"grace_warn,omitempty"` // behind target this long → warn; default 1m
GraceFail Duration `yaml:"grace_fail,omitempty"` // behind target this long → fail; default 2m
DownAfter Duration `yaml:"down_after,omitempty"` // no report this long → down; default 5m
Strict bool `yaml:"strict,omitempty"` // reports from undeclared clients are rejected
Clients map[string]FleetClient `yaml:"clients,omitempty"` // the declared fleet — "know your clients"
}
FleetConfig describes this repository as a source with a fleet.
The health clocks: catching up within GraceWarn is normal (pass); behind target past GraceWarn is warn, past GraceFail is fail; a client not seen for DownAfter is down — silence is its own verdict, above failure.
type FrameType ¶
type FrameType uint8
FrameType is the encoding of one entry's content frame.
const (
FrameZstd FrameType = 0 // whole-version zstd; delta frames reserved for v2
)
type Index ¶
type Index interface {
// Append is the only writer. It assigns cs.Rev = Max()+1 and returns it
// (a preset Rev must equal Max()+1: ErrNotLinear otherwise), writes
// log/<rev>.yml, then advances max_revision.txt.
Append(cs Changeset) (rev uint64, err error)
// Snapshot folds the nearest stored snapshot ≤ rev forward to the
// full directory at rev (all modify, one change per live path); one is
// persisted every K revisions.
Snapshot(rev uint64) ([]Change, error)
// Diff returns the changes bridging src → target: modify with target
// frev where differing or new, delete where gone. (Snapshot of both,
// compared.)
Diff(src, target uint64) ([]Change, error)
// Log returns changesets newest-first, filtered by rev range and/or path.
Log(f LogFilter) ([]Changeset, error)
Current() (uint64, error)
SetCurrent(rev uint64) error // atomic rename of current.txt
Max() (uint64, error) // reads max_revision.txt
// Base is the earliest reachable revision: 0 unless shallow.
// Snapshot, Diff, and Log below it fail with ErrPruned.
Base() (uint64, error)
// InstallBase makes the index shallow at rev in one step: store the
// snapshot as the new base, ensure max ≥ rev, no log records below.
// Used by bootstrap and Prune.
InstallBase(rev uint64, snapshot []Change) error
// Verify checks log numbering is dense above base and snapshots
// replay correctly.
Verify() error
}
Index is the indexing subsystem: what the directory is, per revision. It knows paths, frevs, status, and a 7-char short sha per change — never sizes, storage offsets, or actual bytes. It can run standalone against synthetic changesets with no real files at all.
This set is closed — anything more belongs to the top level.
type IndexConfig ¶ added in v0.0.2
type IndexConfig struct {
// ChunkSize is how many revisions share one log chunk file
// (default 100) — the inode knob.
ChunkSize uint64 `yaml:"chunk_size,omitempty"`
// SnapshotEvery is the snapshot cadence in chunks (default 1): one
// snapshot per this many chunks, always on a chunk boundary, so a
// fold reads at most this many chunk files.
SnapshotEvery uint64 `yaml:"snapshot_every,omitempty"`
}
IndexConfig tunes the indexing subsystem's storage.
type LogFilter ¶
LogFilter selects changesets for Index.Log. Zero values mean unbounded: From 0 = the earliest reachable revision, To 0 = max, Path "" = all paths.
type Orders ¶
type Orders struct {
Target uint64 `json:"target"`
Directive Directive `json:"directive,omitempty"`
RecreateTo uint64 `json:"recreate_to,omitempty"` // set with DirectiveRecreate
}
Orders is the server's response to a report: the client's resolved target and any directive.
type PatchHeader ¶
type PatchHeader struct {
Magic [4]byte // "GGPA"
Version uint16 // format version; currently 1
Flags uint16 // reserved, must be 0
}
PatchHeader opens every entry stream — wire patches and .blob files alike. Same encoding: binary.Write(buf, binary.BigEndian, &hdr). The global header makes every stream self-identifying: format evolution is detected up front, not by failing mid-stream.
func NewPatchHeader ¶
func NewPatchHeader() PatchHeader
NewPatchHeader returns the header for the current format version.
func ReadPatchHeader ¶
func ReadPatchHeader(r io.Reader) (PatchHeader, error)
ReadPatchHeader reads and validates a stream's opening header: a wrong magic is ErrCorrupt, an unsupported version is ErrVersion.
type Remote ¶
type Remote interface {
Max() (uint64, error)
Changesets(from, to uint64) ([]Changeset, error)
Snapshot(rev uint64) ([]Change, error) // bootstrap
Patch(changes []Change) (io.ReadCloser, error) // remote create_patch
}
Remote is the read-only face of any update source. The HTTP API is its wire form; a shared filesystem, an S3 bucket, or a test fake satisfy the same four methods. So does *repo.Repo itself — its read side is a Remote — which lets repos compose into chains (gateway ← hub ← origin) and makes dir-to-dir sync just repoA.Update(repoB).
type Report ¶
type Report struct {
Client string `json:"client" yaml:"client"`
Current uint64 `json:"current" yaml:"current"`
Max uint64 `json:"max" yaml:"max"`
Base uint64 `json:"base" yaml:"base"` // where it starts from (shallowness)
State ClientState `json:"state" yaml:"state"`
Detail string `json:"detail,omitempty" yaml:"detail,omitempty"` // the error when failed
Info map[string]string `json:"info,omitempty" yaml:"info,omitempty"` // free-form: version, uptime, …
}
Report is one client status beat — "who I am, where I am, where I started from, how I feel".
type ReportHandler ¶
ReportHandler is what the server's /report endpoint calls. Defined here so gigit/server never imports fleet; fleet.Tracker implements it.
type Reporter ¶
Reporter is the optional client-side capability: report status, receive orders, in one round trip. remote.Client implements it; *repo.Repo and other read-only Remotes deliberately do not — dir-to-dir chains don't report.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package admin is the gigit backoffice UI: a read-only administrative interface for browsing a repository — overview, history search, revision directories, file contents at any revision, diffs, and the raw workdir (including .gg) — built on go-run's chiutil route folders.
|
Package admin is the gigit backoffice UI: a read-only administrative interface for browsing a repository — overview, history search, revision directories, file contents at any revision, diffs, and the raw workdir (including .gg) — built on go-run's chiutil route folders. |
|
Package blobs implements the blob subsystem: versioned content storage per (path, frev).
|
Package blobs implements the blob subsystem: versioned content storage per (path, frev). |
|
cmd
|
|
|
gg
command
Command gg is the gigit CLI: a minimal revision control system for wholly-owned config/state directories.
|
Command gg is the gigit CLI: a minimal revision control system for wholly-owned config/state directories. |
|
gg-admin
command
Command gg-admin serves the gigit backoffice UI for one repository: a read-only web interface for browsing its state, history, directories, file contents, and diffs.
|
Command gg-admin serves the gigit backoffice UI for one repository: a read-only web interface for browsing its state, history, directories, file contents, and diffs. |
|
examples
|
|
|
admindemo
command
Command admindemo builds a demo repository with a believable config history — adds, modifications, a delete, a bad change and its rollback — then serves the admin backoffice over it.
|
Command admindemo builds a demo repository with a believable config history — adds, modifications, a delete, a bad change and its rollback — then serves the admin backoffice over it. |
|
syncdemo/gateway
command
Command gateway is the client half of the sync demo: a deployed destination running fleet.Follow — poll, pull toward the target, report every beat, obey orders (lock it to a revision or mark it for recreate on the origin's fleet page).
|
Command gateway is the client half of the sync demo: a deployed destination running fleet.Follow — poll, pull toward the target, report every beat, obey orders (lock it to a revision or mark it for recreate on the origin's fleet page). |
|
syncdemo/origin
command
Command origin is the server half of the sync demo: a repository that keeps living.
|
Command origin is the server half of the sync demo: a repository that keeps living. |
|
Package fleet is the client–server cooperation layer: the Tracker is the server's collected view of its clients (reports in, orders out, health and metrics evaluated over the facts), and Follow is the client loop that feeds it.
|
Package fleet is the client–server cooperation layer: the Tracker is the server's collected view of its clients (reports in, orders out, health and metrics evaluated over the facts), and Follow is the client loop that feeds it. |
|
Package index implements the indexing subsystem over one directory: what the directory is, per revision.
|
Package index implements the indexing subsystem over one directory: what the directory is, per revision. |
|
internal
|
|
|
atomicfile
Package atomicfile is the one place the §7 durability discipline is implemented: rename gives ordering, not persistence — fsync the file before its rename, and fsync the containing directory after.
|
Package atomicfile is the one place the §7 durability discipline is implemented: rename gives ordering, not persistence — fsync the file before its rename, and fsync the containing directory after. |
|
webui
Package webui is the shared page chrome for gigit's backoffice pages: one layout, one palette (chiutil's folder index's, dark by default, light under prefers-color-scheme), used by gigit/admin's repository pages and gigit/fleet's admin folder alike.
|
Package webui is the shared page chrome for gigit's backoffice pages: one layout, one palette (chiutil's folder index's, dark by default, light under prefers-color-scheme), used by gigit/admin's repository pages and gigit/fleet's admin folder alike. |
|
Package remote is the HTTP client side of the sync API: a gigit.Remote over the four endpoints, the exact mirror of gigit/server.
|
Package remote is the HTTP client side of the sync API: a gigit.Remote over the four endpoints, the exact mirror of gigit/server. |
|
Package repo is the top level: a Repo binds the indexing and blob subsystems over one folder and carries the top-level verbs — commit, status, log, diff, checkout, update, pull, prune.
|
Package repo is the top level: a Repo binds the indexing and blob subsystems over one folder and carries the top-level verbs — commit, status, log, diff, checkout, update, pull, prune. |
|
Package server exposes any gigit.Remote over HTTP: the four endpoints are exactly the Remote interface, method for method — the transport is an implementation detail, and the usual server is a *repo.Repo with its Remote face exposed.
|
Package server exposes any gigit.Remote over HTTP: the four endpoints are exactly the Remote interface, method for method — the transport is an implementation detail, and the usual server is a *repo.Repo with its Remote face exposed. |