Implement repository hygiene
Add-git seeds a minimal .gitignore (.DS_Store) before the initial stage — the seed rides "Initial board state" and .DS_Store never enters history; an existing .gitignore (or a directory wearing the name) is left alone forever, adoption and repo-nested seed nothing. GitHousekeeping is the periodic loose-object repack: filesystem enumeration of objects/<2hex>/<38hex> (never git_odb_foreach, which would rewrite the whole database into a fresh pack each pass), git_packbuilder_insert one oid at a time, additive pack write — and deletion only after each oid is re-verified against the written pack opened as a standalone one-pack odb with no loose backend. Any failure returns before deleting; the worst case is a stray pack. Nothing prunes, expires, or consolidates — existing packs accumulate, recorded as the accepted cost of never rewriting storage the app didn't write. GitHousekeeper schedules it: git's own 6700 threshold, 8s after session activation (outlasting the launch catch-up), background priority, skipped under pause states, held locks, or an in-flight commit, never retried — the next open tries again. Free tier composes none of it. 20 new tests: full-odb equality, per-oid survival, identical walks, byte+mtime-identical refs/HEAD/working tree, whole-.git identity on every declined pass, and deleting-never-forgets. 2394 tests / 412 suites green; InertGitTests untouched. Closes pro-m1-git-undo. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -22,8 +22,9 @@ import os
|
||||
/// ranker. **The provider binding is not part of it** — both tiers still bind
|
||||
/// `NativeHistoryProvider` (`AppModel.makeHistoryProvider`), and the consumer of `mode` is the
|
||||
/// undo/redo card two cards later, which builds the git `HistoryProviding` implementation over
|
||||
/// exactly this object. Auto-commit, commit messages, branch controls, the identity fields, remotes
|
||||
/// and `.gitignore` seeding are each their own card and deliberately absent here.
|
||||
/// exactly this object. Auto-commit, commit messages, branch controls, the identity fields, the
|
||||
/// `.gitignore` seed and its periodic housekeeping each arrived as their own card and are composed
|
||||
/// here now; remotes are pro-m2's and deliberately still absent.
|
||||
@MainActor
|
||||
@Observable
|
||||
public final class HistoryStore {
|
||||
@@ -87,6 +88,17 @@ public final class HistoryStore {
|
||||
/// `HistoryStore` built without one has a switcher that can list branches and nothing else.
|
||||
public private(set) var switcher: GitBranchSwitcher?
|
||||
|
||||
/// **Periodic safe housekeeping** (06-history-undo.md ▸ Repository hygiene), or `nil` on a board
|
||||
/// there is no repository to maintain.
|
||||
///
|
||||
/// Its existence is exactly `mode == .git`, the committer's rule for the committer's reason, and
|
||||
/// it is composed inert for a sharper version of the committer's: packing loose objects is the
|
||||
/// most expensive thing this layer can do, and board open is where 02-architecture.md's
|
||||
/// hang-avoidance doctrine is strictest. `activateAutoCommit(_:)` is what arms it — beside the
|
||||
/// committer, so the two are one decision — and a `HistoryStore` built without a session has a
|
||||
/// housekeeper that never runs.
|
||||
public private(set) var housekeeper: GitHousekeeper?
|
||||
|
||||
// MARK: - Commit identity
|
||||
|
||||
/// **What repo-local `.git/config` says right now** — the popover's two fields, as values rather
|
||||
@@ -146,6 +158,7 @@ public final class HistoryStore {
|
||||
if mode == .git {
|
||||
committer = GitAutoCommitter(boardRoot: boardRoot, ledger: ledger)
|
||||
switcher = GitBranchSwitcher(boardRoot: boardRoot)
|
||||
housekeeper = GitHousekeeper(boardRoot: boardRoot)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,12 +174,27 @@ public final class HistoryStore {
|
||||
guard let committer else { return }
|
||||
wire(committer)
|
||||
committer.start()
|
||||
armHousekeeping(beside: committer)
|
||||
}
|
||||
|
||||
/// Stops the committer — the session's teardown, so a closed board's debounce cannot fire against
|
||||
/// a store that has gone.
|
||||
/// Stops the committer, and the maintenance beside it — the session's teardown, so neither a
|
||||
/// closed board's debounce nor its housekeeping can fire against a store that has gone.
|
||||
public func stopAutoCommit() {
|
||||
committer?.stop()
|
||||
housekeeper?.cancel()
|
||||
}
|
||||
|
||||
/// **Arms the board-open housekeeping pass** (06 ▸ Repository hygiene) — one call site's worth of
|
||||
/// wiring, shared by the session's activation and by a mid-session add-git, so a board that
|
||||
/// flipped into git mode maintains itself exactly like one that opened in it.
|
||||
///
|
||||
/// The gate it hands over is the committer's own in-flight flag, read at the moment of dispatch:
|
||||
/// the simplest honest way to keep optional work from starting beside the one operation that must
|
||||
/// never be disturbed, and deliberately not a lock — see `GitHousekeeper.runNow()`.
|
||||
private func armHousekeeping(beside committer: GitAutoCommitter) {
|
||||
guard let housekeeper else { return }
|
||||
housekeeper.isCommitInFlight = { [weak committer] in committer?.isCommitInFlight ?? false }
|
||||
housekeeper.schedule()
|
||||
}
|
||||
|
||||
/// **The tier gate and the open-time detection, in one line** (12-editions.md ▸ The provider
|
||||
@@ -242,6 +270,11 @@ public final class HistoryStore {
|
||||
// The branch controls appear with the repository they switch branches in — and before
|
||||
// `didAddGit`, which is what wires their seams (`AppModel.wireGitUndo`).
|
||||
switcher = GitBranchSwitcher(boardRoot: root)
|
||||
// Housekeeping too, for the committer's reason: a board that flipped mid-session behaves
|
||||
// like one that opened in git mode. A repository seconds old has a handful of loose
|
||||
// objects and will read below threshold — which is the pass doing its job, not skipping.
|
||||
housekeeper = GitHousekeeper(boardRoot: root)
|
||||
armHousekeeping(beside: committer)
|
||||
// Last, after the mode and the committer: the undo binding reads both.
|
||||
didAddGit?()
|
||||
Self.logger.notice("add-git initialized a repository at \(root.path, privacy: .public)")
|
||||
|
||||
Reference in New Issue
Block a user