porcelain

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 14, 2026 License: MIT Imports: 39 Imported by: 0

Documentation

Index

Constants

View Source
const AutoSavePrefix = "auto -"

AutoSavePrefix is the message prefix that identifies auto-saved snapshots (created by 'drift watch'). Commands that filter or label auto-saves (e.g. 'log' hiding them by default, 'gc --keep-auto' preserving them) share this constant so the prefix has a single source of truth.

Variables

View Source
var (
	// ErrNotARepo is returned when an operation requires a drift
	// repository but the target directory does not contain a .drift/
	// directory. The CLI maps this to a dedicated exit code so scripts
	// can branch on it without parsing the message.
	ErrNotARepo = errors.New("not a drift repository")

	// ErrNothingToSave is returned when a snapshot is requested but the
	// workspace has no changes since the last snapshot.
	ErrNothingToSave = errors.New("nothing to save")

	// ErrBranchNotFound is returned when a referenced branch does not exist.
	ErrBranchNotFound = errors.New("branch not found")

	// ErrBranchAlreadyExists is returned when creating a branch that already
	// exists.
	ErrBranchAlreadyExists = errors.New("branch already exists")

	// ErrSnapshotNotFound is returned when a referenced snapshot does not
	// exist.
	ErrSnapshotNotFound = errors.New("snapshot not found")

	// ErrAmbiguousID is returned when an id:<prefix> reference matches more
	// than one snapshot. The wrapped error message lists the matching
	// snapshots so callers can surface them to the user.
	ErrAmbiguousID = errors.New("ambiguous snapshot ID prefix")

	// ErrTagAlreadyExists is returned when creating a tag that already exists.
	ErrTagAlreadyExists = errors.New("tag already exists")

	// ErrTagNotFound is returned when a referenced tag does not exist.
	ErrTagNotFound = errors.New("tag not found")

	// ErrCannotUndo is returned when UndoLastSave is called but HEAD is
	// already at the initial snapshot (no previous snapshot to revert to).
	ErrCannotUndo = errors.New("cannot undo: already at initial snapshot")

	// ErrUncommittedChanges is returned when an operation that would lose
	// workspace changes is attempted (e.g. undo with a dirty workspace, or
	// switch --no-autosave with a dirty workspace).
	ErrUncommittedChanges = errors.New("uncommitted changes would be lost")

	// ErrCannotDeleteCurrentBranch is returned when attempting to delete the
	// currently checked-out branch.
	ErrCannotDeleteCurrentBranch = errors.New("cannot delete the current branch")

	// ErrCannotDeleteMain is returned when attempting to delete the 'main'
	// branch, which is protected.
	ErrCannotDeleteMain = errors.New("cannot delete 'main'")

	// ErrCannotRenameMain is returned when attempting to rename the 'main'
	// branch, which is protected.
	ErrCannotRenameMain = errors.New("cannot rename 'main'")

	// ErrFileNotFound is returned when a file path is not found in a
	// snapshot (e.g. 'drift cat <snapshot> <path>' where path is absent).
	ErrFileNotFound = errors.New("file not found in snapshot")

	// ErrLocked is returned by AcquireWorkspaceLock when the workspace
	// lock is held by another live operation. Callers may test for it
	// with errors.Is.
	ErrLocked = errors.New("workspace is locked by another operation")

	// ErrLockLost is returned by TouchWorkspaceLock when the lock has
	// been stolen by another process during a long-running operation.
	// The caller MUST abort immediately: continuing to modify the store
	// while another process holds the lock will cause corruption.
	ErrLockLost = errors.New("workspace lock lost to another process")
)

Sentinel errors for the porcelain (business) layer. Callers use errors.Is to test for specific failure modes; lower layers wrap these with fmt.Errorf("...: %w", ...) to attach context while preserving the sentinel identity.

View Source
var ErrInvalidPath = errors.New("invalid file path")

Functions

func AcquireWorkspaceLock

func AcquireWorkspaceLock(cwd string) error

AcquireWorkspaceLock creates a workspace lock file at .drift/workspace.lock. It coordinates access between workspace-modifying commands (switch, restore) and the watch daemon so the daemon does not observe an inconsistent state (files rewritten but index not yet rebuilt) during a transition.

This lock is NOT reentrant. Calling AcquireWorkspaceLock from within a function that already holds the lock will return ErrLocked. Use the NoLock variants for internal calls.

Acquisition is race-free across processes:

  1. Fast path: O_CREATE|O_EXCL atomic create. Two processes can never both create the lock when none exists.
  2. Stale-replacement path: the would-be acquirer writes a PID-embedded claim file (workspace.lock.claim.<pid>.<rand>) using O_CREATE|O_EXCL, re-checks that the lock is still stale, then atomically renames the claim file onto the lock path. os.Rename is atomic on the same filesystem, so exactly one of the contending processes wins the rename; the loser's rename either fails (Windows) or silently overwrites with its own content (POSIX), but the loser then observes its PID is not the recorded owner and aborts. This closes the cross-process TOCTOU window that the previous WriteFileAtomic-over-stale approach left open, where two processes could both pass isLockStale and both believe they held the lock after their respective temp-file+rename landed.

A lock that cannot be parsed (e.g. an empty file being written concurrently) is treated as held rather than stale, so a writer is never clobbered mid-write.

func AddTag

func AddTag(ctx context.Context, store storage.Storer, cwd string, name string, snapID core.SnapshotID) error

AddTag creates a tag pointing to the given snapshot. It rejects empty names, invalid names, zero-hash targets, unknown snapshots, and names that already exist. The existence check and the ref write are guarded by the workspace lock so that two concurrent AddTag calls for the same name cannot both pass the check (TOCTOU).

func ComputeFileHash

func ComputeFileHash(filePath string) (core.Hash, error)

ComputeFileHash returns the BLAKE3 file hash for filePath by chunking it with the detected engine and hashing the concatenation of chunk hashes. The hash is independent of chunk data layout and matches the hash CreateSnapshot would produce for the same file.

func ComputeRestoreChanges

func ComputeRestoreChanges(ctx context.Context, workDir string, cfg *core.CoreConfig, snapshot *core.Snapshot) (added []core.FileEntry, modified []core.FileEntry, deleted []string, err error)

ComputeRestoreChanges compares the current workspace files against the target snapshot and returns what would change if the snapshot were restored: files in the snapshot but not in the workspace (added), files in both but with different content (modified), and files in the workspace but not in the snapshot (deleted). Modification is detected by comparing content hashes (BLAKE3) rather than modtime, since tools like "cp -p" preserve modtime while changing content.

func CountFileLines

func CountFileLines(ctx context.Context, store storage.Storer, entry core.FileEntry) int

CountFileLines returns the total number of lines in the file represented by entry, by counting newline bytes across all of its chunks.

Error handling: a missing chunk or a cancelled context causes the function to return 0. This silent fallback is intentional for the current callers (log -v / log --json), which treat line counting as best-effort decoration and prefer a missing count over aborting the whole log listing. Callers that need explicit error handling should use CountFileLinesWithError.

func CountFileLinesWithError

func CountFileLinesWithError(ctx context.Context, store storage.Storer, entry core.FileEntry) (int, error)

CountFileLinesWithError is like CountFileLines but returns an error when a chunk cannot be read or the context is cancelled, so callers that need to distinguish "zero lines" from "failed to count" can do so.

func CountSnapshotChanges

func CountSnapshotChanges(ctx context.Context, store storage.Storer, summary *core.SnapshotSummary) (added, modified, deleted int)

CountSnapshotChanges loads the snapshot referenced by summary and returns the added/modified/deleted file counts relative to its parent. Errors are logged and zero counts are returned so that a single failure does not abort the whole log listing.

func CountUnreachableSnapshots

func CountUnreachableSnapshots(ctx context.Context, store storage.Storer, workDir string) (int, error)

CountUnreachableSnapshots returns the number of snapshots that are not reachable from any branch or tag reference.

Like CollectGarbage, it acquires the workspace lock so it does not observe a half-applied save/switch/restore that would produce a misleading count.

func CreateBranch

func CreateBranch(ctx context.Context, store storage.Storer, cwd, name string) (core.SnapshotID, error)

CreateBranch creates a new branch pointing at the current HEAD snapshot. Returns the tip snapshot ID (zero if HEAD has no commits yet).

func CreateSnapshot

func CreateSnapshot(ctx context.Context, store storage.Storer, workDir string, message string, author string, cfg *core.CoreConfig) (*core.Snapshot, error)

CreateSnapshot scans workDir, chunks new or modified files, stores them, and writes a new snapshot on the current branch (HEAD's symbolic target, defaulting to heads/main). The workspace lock is acquired for the duration of the save. message must be non-empty; author defaults to "drift" when empty; cfg may be nil (core.DefaultConfig is used). Returns ErrNothingToSave when the workspace matches the index.

Tags are NOT written into the snapshot. Tags live exclusively as tags/<name> refs (created by cmd/save.go via AddTag after the snapshot exists), which keeps them mutable — 'tag delete' and 'tag rename' actually take effect on the log view, instead of being frozen into the immutable snapshot. Old snapshots with embedded Tags fields are still readable; ResolveTagTips + mergeTags in the log layer merges both sources so historical data is preserved.

func DeleteBranch

func DeleteBranch(ctx context.Context, store storage.Storer, cwd, name string) error

DeleteBranch removes a branch reference. It refuses to delete:

  • "main" (the default, protected branch)
  • the current branch (user must switch away first)

Only the reference is removed; snapshots remain in storage and become unreachable if no other branch or tag references them. Run `drift gc --dry-run` to review unreachable snapshots, then `drift gc` to reclaim the disk space.

func DeleteTag

func DeleteTag(ctx context.Context, store storage.Storer, cwd string, name string) error

DeleteTag removes a tag by name. It returns ErrTagNotFound if the tag does not exist. The existence check and the ref delete are guarded by the workspace lock so that two concurrent DeleteTag calls cannot race on the same name (TOCTOU).

func DetectFileTypeLabel

func DetectFileTypeLabel(ctx context.Context, store storage.Storer, entry *core.FileEntry) string

DetectFileTypeLabel returns a human-readable label for the file type of the given snapshot file entry, by reading its chunks from storage and detecting the filetype engine. Image files include parsed dimensions when available. It returns "binary" when the chunks cannot be read or no engine matches.

func ImportFileFromBranch

func ImportFileFromBranch(ctx context.Context, store storage.Storer, workDir, branchName, filePath string, cfg *core.CoreConfig) (*core.FileEntry, error)

ImportFileFromBranch reconstructs a single file from the target branch's HEAD snapshot and writes it to the current workspace at the same relative path. The workspace lock is acquired for the duration. The index is NOT updated: the imported file appears as a new (added) file in 'drift status' and is captured by the next 'drift save', which is the intended workflow.

This is a non-merge file-level cherry-pick: it does not touch any other workspace files, does not move HEAD, and does not create a snapshot. It is useful for bringing a single file from an experimental branch into the current branch without switching.

Returns the imported FileEntry and an error. ErrBranchNotFound is returned when the branch does not exist; ErrSnapshotNotFound when the branch has no snapshots; ErrFileNotFound when the file is not present in the branch's HEAD snapshot.

func InitProject

func InitProject(path string) error

InitProject initializes a new drift repository at the given path using the default on-disk storage backend. It is a thin wrapper around InitProjectWithFactory with defaultStoreFactory.

func InitProjectWithFactory

func InitProjectWithFactory(path string, factory StoreFactory) error

InitProjectWithFactory initializes a new drift repository at the given path, using the provided StoreFactory to construct the storage backend. The factory is the seam for injecting a non-default backend in tests.

func ListBranches

func ListBranches(ctx context.Context, store storage.Storer) ([]*core.Reference, string, error)

ListBranches returns all branch references and the name of the current branch (without the "heads/" prefix). If HEAD is not a symbolic reference the current branch name is empty.

func LsRemote

func LsRemote(ctx context.Context, workDir, remoteName string) ([]*core.Reference, error)

LsRemote lists all refs from a remote without downloading any objects.

func OpenProject

func OpenProject(path string) (storage.Storer, *core.Config, error)

OpenProject opens an existing drift repository at the given path using the default on-disk storage backend, returning the store, the loaded config, and any error. It is a thin wrapper around OpenProjectWithFactory with defaultStoreFactory.

func OpenProjectWithFactory

func OpenProjectWithFactory(path string, factory StoreFactory) (storage.Storer, *core.Config, error)

OpenProjectWithFactory opens an existing drift repository at the given path, using the provided StoreFactory to construct the storage backend. The factory is the seam for injecting a non-default backend in tests.

func PullDryRun

func PullDryRun(ctx context.Context, store storage.Storer, workDir, remoteName, branch string, all bool) (*remote.SyncStats, error)

PullDryRun returns what would be pulled without downloading. It acquires the workspace lock so the dry-run reflects a consistent local state.

func PushDryRun

func PushDryRun(ctx context.Context, store storage.Storer, workDir, remoteName, branch string, all bool) (*remote.SyncStats, error)

PushDryRun returns what would be pushed without uploading. It acquires the workspace lock so the dry-run reflects a consistent local state.

func RebuildIndexFromSnapshot

func RebuildIndexFromSnapshot(ctx context.Context, store storage.Storer, snapID core.SnapshotID) error

RebuildIndexFromSnapshot regenerates the staging index from a snapshot's file entries. Used by switch (snapshot_branch.go) and pull (sync.go) after the branch tip changes, so the index reflects the new tip rather than the old workspace state. save.go and restore.go do not use this — save builds the index from freshly chunked fileEntries, and restore filters out failed files via failedSet, so they keep their own inline loops.

func ReleaseWorkspaceLock

func ReleaseWorkspaceLock(cwd string)

ReleaseWorkspaceLock removes the workspace lock file, but only if it is owned by the current process. This prevents a process from clobbering a lock that another operation has acquired after this process stopped using it (e.g. a stale lock that was stolen and refreshed between acquire and release). The error is intentionally ignored: if the lock is already gone or is no longer owned by us there is nothing useful for callers to do, and a leftover lock will be reclaimed by the stale-timeout in AcquireWorkspaceLock.

func RemoveStalePidFile

func RemoveStalePidFile(cwd string, pid int)

RemoveStalePidFile removes watch.pid if it contains the given PID. This is used by the _watch_daemon child process to clean up after an init failure (before RunDaemonLoop installs its own cleanup handlers), so that a subsequent `drift watch status` does not see a stale PID pointing at an already-exited process. The PID check avoids clobbering a different daemon that won the start race.

func RenameBranch

func RenameBranch(ctx context.Context, store storage.Storer, cwd, oldName, newName string) error

RenameBranch renames a branch from oldName to newName. It refuses to rename:

  • "main" (the default, protected branch)
  • a non-existent branch
  • to a name that already exists

If the renamed branch is the current branch (HEAD points to it), HEAD is updated to point to the new name. Only references are modified; snapshots remain untouched.

The operation is ordered as SetRef(new) then DeleteRef(old) so that a crash leaves a duplicate rather than a missing branch, which is safer to recover.

func RenameTag

func RenameTag(ctx context.Context, store storage.Storer, cwd string, oldName, newName string) error

RenameTag renames an existing tag. The new name is NFC-normalized and validated before use. The operation is ordered as SetRef(new) then DeleteRef(old) so a crash leaves a duplicate rather than a missing tag, mirroring RenameBranch's safety property. The existence checks and ref writes are guarded by the workspace lock so that concurrent RenameTag calls cannot race on the same names (TOCTOU).

func ResolveBranchTips

func ResolveBranchTips(ctx context.Context, store storage.Storer) (map[string][]string, error)

ResolveBranchTips returns a map from snapshot hash to the list of branch names whose tip (Target) points directly at that snapshot. A snapshot that is not the tip of any branch gets no entry.

This mirrors git's --decorate=short behavior: the branch column in 'log' shows where each branch head sits, leaving the rest of the chain unlabeled so the user can see at a glance where branches diverge.

The returned branch names are sorted alphabetically for stable display.

func ResolveCurrentBranchName

func ResolveCurrentBranchName(ctx context.Context, store storage.Storer) string

ResolveCurrentBranchName returns the name of the current branch (without the "heads/" prefix), or "" if HEAD is detached or unreadable.

func ResolveHeadSnapshot

func ResolveHeadSnapshot(ctx context.Context, store storage.Storer) *core.Snapshot

ResolveHeadSnapshot returns the HEAD snapshot, or nil if none exists.

When HEAD is a symbolic reference to a branch, the branch's target snapshot is returned. Storer.GetRef is contractually required to fully resolve symrefs: a GetRef("HEAD") call on a symref HEAD must return a Reference whose Target is the final snapshot hash (not the intermediate branch ref). This matches the documented contract of storage.Storer.GetRef and the filesystem backend's implementation in internal/storage/backends/filesystem/ref.go. Callers therefore never need a second GetRef to chase symrefs. resolveHead (in resolve.go) relies on the same contract; the two functions intentionally use a single GetRef.

func ResolveSnapshotRef

func ResolveSnapshotRef(ctx context.Context, store storage.Storer, id string) (*core.Snapshot, error)

ResolveSnapshotRef resolves a snapshot reference to a snapshot.

Snapshot reference syntax (see docs/cli-design.md "版本引用语法"):

  • id:<hash-prefix> — match by snapshot hash prefix (>= minHashPrefixLen chars)
  • tag:<name> — resolve via tags/<name> reference
  • branch:<name> — resolve via heads/<name> reference (branch head)
  • head — current HEAD snapshot
  • <bare-name> — equivalent to branch:<bare-name>

The colon-prefixed syntax replaces the earlier @-prefixed form (@id:..., @tag:..., @branch:..., @head) which collided with PowerShell's splat operator and required quoting on Windows. Colons are ordinary characters in all common shells (PowerShell, bash, zsh, fish, cmd) and are already rejected by refname.Validate, so they cannot appear in branch or tag names.

Returns ErrSnapshotNotFound if the referenced snapshot does not exist. Returns an error wrapping ErrAmbiguousID if the hash prefix matches more than one snapshot (the message lists the matching short IDs). Returns an error if the hash prefix is shorter than minHashPrefixLen.

func ResolveTagTips

func ResolveTagTips(ctx context.Context, store storage.Storer) (map[string][]string, error)

ResolveTagTips returns a map from snapshot hash to the list of tag names whose Target points directly at that snapshot. A snapshot with no tags gets no entry.

Tags live exclusively as `tags/<name>` refs: `drift save --tag` creates refs after the snapshot is written, `drift tag add` creates a ref pointing at an existing snapshot, and `tag delete`/`tag rename` mutate refs. New snapshots no longer embed a Tags field, so refs are the authoritative source — this function reads them so the log view reflects the current tag state regardless of when tags were attached. Old snapshots with embedded Tags fields are merged in by the log layer's mergeTags for backward compatibility.

The returned tag names are sorted alphabetically for stable display.

func RestoreSnapshot

func RestoreSnapshot(ctx context.Context, store storage.Storer, workDir string, snapshotID core.SnapshotID, filePath string, noBackup bool, cfg *core.CoreConfig) (backupID string, err error)

RestoreSnapshot restores files from snapshotID into workDir. When filePath is empty the entire snapshot is restored (workspace files absent from the snapshot are removed); otherwise only that single file is restored and the index is updated for it. When noBackup is false a backup snapshot of the current workspace is created first and its short ID is returned in backupID (empty when no backup was needed, e.g. ErrNothingToSave). cfg may be nil (core.DefaultConfig is used). The named return err is wrapped by a defer so that on failure the backup ID (if any) is appended for rollback guidance.

Rollback strategy: restore is not transactional across multiple files. Per-file atomicity is provided by writeFileFromChunks (temp file + rename), so a write failure never leaves a half-written file. When a restore fails partway, restoreFilesToWorkspace skips the cleanup phase (deletion of non-snapshot files) and updates the index to reflect only successfully restored entries, so the workspace stays as consistent as possible. The backup snapshot (when enabled) captures the pre-restore workspace state so the user can manually roll back with `drift restore <backupID>`.

func RunDaemonLoop

func RunDaemonLoop(ctx context.Context, store storage.Storer, cwd string, interval int, keep int, cfg *core.CoreConfig)

RunDaemonLoop runs the watch daemon loop. It periodically detects workspace changes and creates auto-snapshots when changes are found. It prunes old auto-snapshots to keep at most `keep` entries.

func SetConfigValue

func SetConfigValue(ctx context.Context, store storage.Storer, cfg *core.Config, key, value string) error

SetConfigValue writes a user-configurable key into cfg and persists the updated config to storage. Only user-facing keys (user.name, user.email) are accepted; algorithm tuning parameters (chunk sizes, compression) are intentionally not exposed — they are hardcoded in core.DefaultConfig and should not be tuned by end users. Returns an error if the key is unknown.

func SnapshotFileDiff

func SnapshotFileDiff(ctx context.Context, store storage.Storer, snapshot *core.Snapshot) (added []core.FileEntry, modified []core.FileEntry, deleted []string, err error)

SnapshotFileDiff diffs the given snapshot against its predecessor, returning the added, modified, and deleted file sets. When the snapshot has no predecessor (initial snapshot), every file is treated as added.

Modification is detected by comparing file Hash (BLAKE3), consistent with countSnapshotDiff. Hash changes iff size or chunk list changes, so this is equivalent to comparing (Size, Chunks) but simpler.

func SortSnapshotSummariesNewestFirst

func SortSnapshotSummariesNewestFirst(snaps []*core.SnapshotSummary)

SortSnapshotSummariesNewestFirst sorts snapshot summaries in reverse chronological order (newest first), using the PrevID chain depth as a secondary sort key when timestamps are equal.

Primary sort key is timestamp (descending). When timestamps are equal (rapid successive saves), it uses the PrevID chain: if A.PrevID == B.ID then A is newer than B. This is stable for unrelated summaries.

func StartDaemon

func StartDaemon(ctx context.Context, cwd string, interval int, keep int) (int, error)

StartDaemon starts a background watch daemon for the project at cwd. It returns the PID of the started process.

func StopDaemon

func StopDaemon(ctx context.Context, cwd string) (int, int, error)

StopDaemon stops the watch daemon for the project at cwd. Returns the number of auto-saves created and snapshots pruned during the session.

func SwitchBranch

func SwitchBranch(ctx context.Context, store storage.Storer, workDir string, name string, create, noAutosave bool, author string, cfg *core.CoreConfig) (string, string, int, error)

SwitchBranch switches to the target branch. If create is true, it creates the branch first. It auto-saves current changes, updates HEAD symref, and restores the target snapshot to workspace. Returns autosave snapshot short ID (empty if nothing to save), the source branch name, and the number of files that differ between the source and target branch snapshots.

When noAutosave is true, the auto-save step is skipped and the workspace must be clean (no uncommitted changes); otherwise ErrUncommittedChanges is returned. This supports the 'drift switch --no-autosave' flow for users who have already manually saved and want to avoid an extra [auto] snapshot.

func TouchWorkspaceLock

func TouchWorkspaceLock(workDir string) error

TouchWorkspaceLock refreshes the workspace lock's timestamp to prevent it from being considered stale during a long-running operation. It should be called periodically by operations that may exceed lockStaleTimeout (e.g. snapshotting a very large workspace).

If the lock has been stolen by another process (PID mismatch), TouchWorkspaceLock returns ErrLockLost. The caller MUST abort the operation immediately — continuing to modify the store while another process holds the lock will cause corruption. A missing or unparseable lock file is treated as ErrLockLost as well, since the lock should not disappear while the operation holds it.

func UndoLastSave

func UndoLastSave(ctx context.Context, store storage.Storer, workDir string, cfg *core.CoreConfig) error

UndoLastSave reverts the last save operation by moving HEAD back to the previous snapshot. The undone snapshot becomes unreachable (will be collected by gc). It refuses if there are uncommitted workspace changes.

If HEAD is a symbolic reference to a branch, the branch's target is moved back. If HEAD is detached, HEAD's own target is moved back. The workspace files are not touched; the index is rebuilt from the previous snapshot so that subsequent status/save operations reflect the new HEAD.

Workspace sync: the on-disk files still reflect the undone snapshot. To make the workspace match the new HEAD, run `drift restore <prevID>` after a successful undo. Without this, `drift status` will report the workspace as "dirty" (the files match the old HEAD, not the new one).

func WalkSnapshotChain

func WalkSnapshotChain(ctx context.Context, store storage.Storer, startHash core.Hash) ([]*core.SnapshotSummary, error)

WalkSnapshotChain walks the PrevID chain starting from startHash and returns snapshot summaries in chain order (newest first). The walk stops at the first missing snapshot, a nil PrevID, or a zero PrevID hash. It is context-cancellable.

This is the core of 'drift log' default and --branch modes: by walking only the current branch's chain, inherited commits from parent branches are included (matching git log semantics), giving the user the full evolution history of the branch.

Types

type ChangeSummary

type ChangeSummary struct {
	Added    []string
	Modified []string
	Deleted  []string
	// UntrackedSymlinks is the number of symbolic links present in the
	// workspace. Drift's snapshot schema cannot represent symlink targets,
	// so symlinks are silently skipped during save and are never restored.
	// This count surfaces them so the CLI can warn the user that those
	// entries are not under version control.
	UntrackedSymlinks int
}

ChangeSummary summarizes workspace changes since last save.

func DetectChanges

func DetectChanges(ctx context.Context, store storage.Storer, workDir string, cfg *core.CoreConfig) (*ChangeSummary, error)

DetectChanges compares the workspace against the stored index and returns changes.

It acquires the workspace lock so that the workspace scan and the index it is compared against cannot be mutated mid-comparison by a concurrent save, switch, or restore (which would otherwise produce a tear: half the files from the old state, half from the new).

type ChunkRef

type ChunkRef struct {
	SnapID   string
	FilePath string
	Idx      int
	Hash     core.Hash
	Status   ChunkStatus
}

ChunkRef records the (snapshot, file, index) context of a chunk reference, for verbose output that identifies where each chunk lives.

type ChunkStatus

type ChunkStatus string

ChunkStatus describes the verification result of a single chunk.

const (
	ChunkOK      ChunkStatus = "OK"
	ChunkCorrupt ChunkStatus = "CORRUPT (hash mismatch)"
	ChunkMissing ChunkStatus = "MISSING"
)

type CloneOptions

type CloneOptions struct {
	TargetDir  string
	WorkDir    string
	RemoteURL  string
	RemoteType string
	User       string
	Password   string
	// RemoteName is the name under which the cloned remote is registered
	// in .drift/remotes.json and credentials.json. Defaults to "origin"
	// when empty. Allowing the caller to choose the name prevents the
	// second clone of the same URL with different credentials from
	// silently overwriting the first remote's stored password.
	RemoteName string
}

CloneOptions holds all parameters for CloneRemote.

type CloneResult

type CloneResult struct {
	Dir              string
	Snapshots        int
	Branches         int
	Tags             int
	Branch           string
	CredentialsSaved bool
}

CloneResult reports the outcome of a clone operation.

func CloneRemote

func CloneRemote(ctx context.Context, opts CloneOptions) (*CloneResult, error)

CloneRemote downloads a remote drift repository into a new directory.

type ContentDiffResult

type ContentDiffResult struct {
	Stdout string
	Stderr string

	// Kind classifies the result for structured consumers: "added",
	// "deleted", "unchanged", "text", or "binary". Empty when the result
	// is a warning/error with no classification.
	Kind string
	// Diff holds the raw diff text produced by the engine for text files.
	// Empty for non-text results.
	Diff string
	// OldSize and NewSize are the byte sizes of the old and new versions.
	// For an added file OldSize is zero; for a deleted file NewSize is zero.
	OldSize int64
	NewSize int64
	// OldDimensions and NewDimensions hold image dimension strings (e.g.
	// "1920x1080") for image files. Empty for non-image results.
	OldDimensions string
	NewDimensions string
}

ContentDiffResult holds the output of a content-level diff for a single file. Stdout is the content to print to stdout (diff text, metadata, or status messages like "(no change)"). Stderr is a warning/hint to print to stderr (empty when none).

The structured fields (Kind, Diff, OldSize, NewSize, OldDimensions, NewDimensions) carry machine-readable metadata for JSON output. The text renderer (cmd.printContentDiff) ignores them and only consumes Stdout / Stderr; JSON renderers read them to build a structured envelope without re-running the diff algorithm.

func DiffFileInSnapshots

func DiffFileInSnapshots(ctx context.Context, store storage.Storer, workDir string, snap1, snap2 *core.Snapshot, filePath string) ContentDiffResult

DiffFileInSnapshots computes a content-level diff for a single file between two snapshots. Both versions are streamed from chunks; the engine is selected from the snap2 header. It returns the diff content (and any warning) without printing; the caller renders the result.

func DiffWorkspaceFileVsSnapshot

func DiffWorkspaceFileVsSnapshot(ctx context.Context, store storage.Storer, workDir string, snapshot *core.Snapshot, filePath string) (ContentDiffResult, error)

DiffWorkspaceFileVsSnapshot computes a content-level diff for a single file: workspace vs snapshot. The workspace file is opened with os.Open and streamed through the engine, and snapshot content is streamed from chunks via a chunkReader, so the file bytes are never buffered whole in memory. It returns the diff content (and any hint) without printing; the caller renders the result.

type ExportResult

type ExportResult struct {
	FileCount int
	TotalSize int64
}

ExportResult reports the outcome of a snapshot export operation.

func ExportSnapshot

func ExportSnapshot(ctx context.Context, store storage.Storer, snapID core.SnapshotID, outputPath string) (*ExportResult, error)

ExportSnapshot reconstructs all files from the given snapshot and writes them to a zip archive at outputPath. Files are streamed chunk-by-chunk into the zip writer, so peak memory is bounded by the largest chunk rather than the largest file. Directory entries are preserved.

The output path's parent directory is created if it does not exist. If the output file already exists it is overwritten.

type FileDiffResult

type FileDiffResult struct {
	Added    []string
	Modified []string
	Deleted  []string
}

FileDiffResult holds a file-level diff between two versions: lists of added, modified, and deleted file paths.

func DiffSnapshots

func DiffSnapshots(snap1, snap2 *core.Snapshot) FileDiffResult

DiffSnapshots computes a file-level diff between two snapshots: files added, modified, or deleted going from snap1 to snap2. It returns the classified file lists without printing; the caller renders the result.

func DiffWorkspaceVsSnapshot

func DiffWorkspaceVsSnapshot(ctx context.Context, workDir string, snapshot *core.Snapshot, cfg *core.CoreConfig) (FileDiffResult, error)

DiffWorkspaceVsSnapshot computes a workspace-vs-snapshot diff: files added, modified, or deleted relative to the snapshot. The workspace is walked once; each file is classified by size (and by content hash on size match). It returns the classified file lists without printing; the caller renders the result.

type FileStat

type FileStat struct {
	Path       string
	Insertions int
	Deletions  int
	Binary     bool
	OldSize    int64
	NewSize    int64
}

FileStat holds per-file change statistics for --stat output. Path is the workspace-relative file path. Insertions and Deletions are the unified-diff line counts (zero for binary files). Binary is true for non-text files or read errors, where Ins/Del are not meaningful. OldSize and NewSize are the byte sizes before and after the change (whichever side exists).

func ComputeStatSnapshots

func ComputeStatSnapshots(ctx context.Context, store storage.Storer, snap1, snap2 *core.Snapshot) ([]FileStat, error)

ComputeStatSnapshots computes per-file change statistics between two snapshots without printing. It is the structured counterpart to DiffSnapshots: where DiffSnapshots classifies files into added/modified/ deleted buckets, ComputeStatSnapshots also reads chunk data for modified files and runs the text engine to count insertions/deletions. The caller renders the result.

func ComputeStatWorkspace

func ComputeStatWorkspace(ctx context.Context, store storage.Storer, workDir string, cfg *core.CoreConfig, snap *core.Snapshot) ([]FileStat, error)

ComputeStatWorkspace computes per-file change statistics between the workspace and a snapshot without printing. It walks the workspace via fsutil.Walk so that the .drift/ directory and ignore-file patterns are honored, and compares same-size files by BLAKE3 hash so that content-only changes are not silently skipped. The caller renders the result.

type FileViewResult

type FileViewResult struct {
	Path       string
	Kind       string
	Size       int64
	ModTime    int64
	Dimensions string
	Content    []byte
}

FileViewResult holds the content and metadata of a file read from a snapshot.

func ReadSnapshotFile

func ReadSnapshotFile(ctx context.Context, store storage.Storer, snapshot *core.Snapshot, workDir, filePath string) (FileViewResult, error)

ReadSnapshotFile reads the full content of a file from a snapshot, reassembling its chunks and detecting the file type.

type GCReport

type GCReport struct {
	SnapshotsRemoved int
	ChunksRemoved    int
	FreedBytes       int64
	AutoKept         int
	LoosePacked      int
	PacksRewritten   int
}

GCReport describes the outcome of a garbage collection pass.

func CollectGarbage

func CollectGarbage(ctx context.Context, store storage.Storer, workDir string, dryRun bool, keepAuto int) (GCReport, error)

CollectGarbage removes snapshots and chunks that are no longer reachable from any branch or tag reference. When dryRun is true nothing is deleted; the report reflects what would be reclaimed. FreedBytes is computed in both modes (best-effort via GetChunk) and is an estimate when dryRun.

keepAuto preserves the N most recent unreachable [auto] snapshots (those whose message starts with the auto-save prefix) from deletion, acting as a safety net against accidental data loss. Their chunks are also kept.

GC does not touch workspace files, but it must not run concurrently with CreateSnapshot: a save in progress may be about to link a chunk that GC would otherwise delete as unreachable. Acquiring the workspace lock serializes GC against save/switch/restore, which are the only operations that add new chunks or snapshots.

type IntegrityReport

type IntegrityReport struct {
	TotalBlocks      int
	Corrupt          int
	Missing          int
	SnapshotCorrupt  int
	FileHashMismatch int
	VerboseRefs      []ChunkRef // populated only when verbose=true
}

IntegrityReport contains the results of a full repository integrity check.

func VerifyIntegrity

func VerifyIntegrity(ctx context.Context, store storage.Storer, workDir, filter string, verbose bool) (*IntegrityReport, error)

VerifyIntegrity verifies the integrity of all chunks in the repository by recomputing their BLAKE3 hashes. It collects unique chunk hashes from all snapshots, verifies each once, and returns a structured report. When filter is non-empty, only files matching the glob pattern are checked. When verbose is true, per-chunk references are collected for detailed output.

workDir is accepted for API consistency with other porcelain functions that operate on a workspace. The current verification logic is repository-wide and does not constrain checks to workDir, but the parameter is retained so future per-workspace scoping can be added without breaking callers.

type PullResult

type PullResult struct {
	Stats *remote.SyncStats
}

PullResult reports the outcome of a pull operation.

func PullFromRemote

func PullFromRemote(ctx context.Context, store storage.Storer, workDir, remoteName, branch string, all bool) (*PullResult, error)

PullFromRemote downloads remote objects to local. It acquires the workspace lock for the duration of the pull because pull writes to the local store and may rebuild the index.

type PushResult

type PushResult struct {
	Stats *remote.SyncStats
}

PushResult reports the outcome of a push operation.

func PushToRemote

func PushToRemote(ctx context.Context, store storage.Storer, workDir, remoteName, branch string, all bool) (*PushResult, error)

PushToRemote uploads local objects to the named remote. It acquires the workspace lock for the duration of the push so that concurrent workspace-modifying operations (snapshot, switch, restore) do not observe an inconsistent local state while push is reading from it.

type StoreFactory

type StoreFactory func(driftPath string) (storage.Storer, error)

StoreFactory builds a storage.Storer rooted at the given .drift path. It is the seam used by InitProjectWithFactory / OpenProjectWithFactory to inject a non-default backend (e.g. the in-memory backend in tests).

type TagInfo

type TagInfo struct {
	Name    string
	Target  core.SnapshotID
	Message string
	Time    time.Time
}

TagInfo is the user-facing representation of a tag, augmented with the target snapshot's message and timestamp so callers can display a tag list without a second round-trip per tag.

func ListTags

func ListTags(ctx context.Context, store storage.Storer) ([]TagInfo, error)

ListTags returns all tags sorted by name. Each TagInfo is enriched with the target snapshot's message and timestamp when the snapshot is still reachable; dangling tags (whose snapshot has been gc'd) get an empty message and zero time.

type WatchState

type WatchState struct {
	StartTime       int64  `json:"start_time"`
	Interval        int    `json:"interval"`
	AutoSaves       int    `json:"auto_saves"`
	MaxSaves        int    `json:"max_saves"`
	LastSaveTime    int64  `json:"last_save_time"`
	LastSaveChanges string `json:"last_save_changes"`
	Pruned          int    `json:"pruned"`
	LastError       string `json:"last_error"`
	Paused          bool   `json:"paused"`
}

WatchState summarizes the runtime state of a watch daemon.

func DaemonStatus

func DaemonStatus(ctx context.Context, cwd string) (*WatchState, bool, error)

DaemonStatus checks whether a watch daemon is running for the project at cwd. If the daemon is alive, it returns the state and true. If the daemon is not running, it cleans up stale files and returns nil, false, nil.

func PauseDaemon

func PauseDaemon(ctx context.Context, cwd string) (*WatchState, error)

PauseDaemon marks the running daemon as paused by setting the Paused flag in the watch state file. The daemon loop re-reads this flag at the start of each tick and skips detection while paused. Returns an error if no daemon is running or the daemon is already paused.

func ResumeDaemon

func ResumeDaemon(ctx context.Context, cwd string) (*WatchState, error)

ResumeDaemon clears the Paused flag in the watch state file so the running daemon resumes detection on its next tick. Returns an error if no daemon is running or the daemon is not paused.

Jump to

Keyboard shortcuts

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