Build branch switching and the popover git surface
GitBranchSwitcher holds 06's sequence as one object: settle editors
explicitly (SessionSettleGate — Save All applies raw buffers with
validation and a refused buffer cancels the whole switch; Discard
reverts buffers AND reconciles the session folders against HEAD;
never silent), flush the pending auto-commit, stamp intent in the
per-board registry, bracketed safe checkout (git_checkout_tree
GIT_CHECKOUT_SAFE + set_head — no path passes FORCE, abort
included), one reload via the async wholesale bracket (failed final
reload engages the existing read-only lock), reseed undo/redo from
the new HEAD with redo empty, clear the stamp. Create-and-switch
keeps the full sequence — the tree-cannot-change proof fails under
concurrent writers. Lock contention shows the 02 in-progress row's
waiting state ("waiting for another writer's git lock"), bounded at
30s then failing cleanly naming the lock path.
GitOperationStamp + GitOperationRecovery: the own-leftovers rule as
a pure conjunction — pause state AND matching stamp = the app's own
interrupted operation, aborted to the pre-operation state with a
banner, stamp cleared on success only; either alone defers to the
pause-and-defer stance. Checked where the committer starts.
BoardGitControls replaces the read-only branch line: branch picker,
inline create-and-switch, the abnormal-state pause note in 06's own
words with controls dimmed, and commit-identity fields that read and
write repo-local .git/config (derived default as placeholder, never
value; unfocused resync, focused keystrokes kept; 2s poll while
visible — .git is watcher-filtered by design).
Also fixes a shipped bug from the undo card: plan(reconciling:)
matched card ids as path prefixes, so the reconcile branch was inert
on every board (<lane>/<card> never matches a bare id) — a session
file the restore diff couldn't name (attachment, comment, draft)
survived Discard and landed in the next flush's commit. One shared
component-exact folder-name resolver now serves both Discard paths;
noteDiscarded takes cardFolderName; regression test verified failing
against the pre-fix code.
41 branch tests + the regression; 2374 tests / 409 suites green;
InertGitTests untouched.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -158,6 +158,16 @@ public struct BoardRecord: Codable, Sendable, Equatable, Identifiable {
|
||||
/// `UserDefaults`.
|
||||
public var remoteLocationWarned: Bool
|
||||
|
||||
/// **A bracketed git operation this app started and has not finished** (06-history-undo.md ▸ Rules
|
||||
/// ▸ Abnormal repo states: "every bracketed operation stamps its intent app-side (per-board
|
||||
/// registry) before touching the repo").
|
||||
///
|
||||
/// `nil` for every board that is not mid-operation, which is every board almost all of the time:
|
||||
/// the stamp is written immediately before the repository is touched and cleared as soon as the
|
||||
/// operation is over, so finding one at open means the app died in between. See
|
||||
/// `GitOperationStamp` for why it lives here rather than in the board folder or under `.git`.
|
||||
public var gitOperationStamp: GitOperationStamp?
|
||||
|
||||
public init(
|
||||
id: UUID = UUID(),
|
||||
bookmark: Data,
|
||||
@@ -172,7 +182,8 @@ public struct BoardRecord: Codable, Sendable, Equatable, Identifiable {
|
||||
pushOnCommit: Bool = false,
|
||||
remoteLocationWarned: Bool = false,
|
||||
icon: String? = nil,
|
||||
iconColor: String? = nil
|
||||
iconColor: String? = nil,
|
||||
gitOperationStamp: GitOperationStamp? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.bookmark = bookmark
|
||||
@@ -188,6 +199,7 @@ public struct BoardRecord: Codable, Sendable, Equatable, Identifiable {
|
||||
self.remoteLocationWarned = remoteLocationWarned
|
||||
self.icon = icon
|
||||
self.iconColor = iconColor
|
||||
self.gitOperationStamp = gitOperationStamp
|
||||
}
|
||||
|
||||
// MARK: Codable
|
||||
@@ -215,6 +227,7 @@ public struct BoardRecord: Codable, Sendable, Equatable, Identifiable {
|
||||
case iconColor
|
||||
case pushOnCommit
|
||||
case remoteLocationWarned
|
||||
case gitOperationStamp
|
||||
}
|
||||
|
||||
public init(from decoder: any Decoder) throws {
|
||||
@@ -233,6 +246,11 @@ public struct BoardRecord: Codable, Sendable, Equatable, Identifiable {
|
||||
iconColor = try container.decodeIfPresent(String.self, forKey: .iconColor)
|
||||
pushOnCommit = try container.decodeIfPresent(Bool.self, forKey: .pushOnCommit) ?? false
|
||||
remoteLocationWarned = try container.decodeIfPresent(Bool.self, forKey: .remoteLocationWarned) ?? false
|
||||
// Tolerant twice over: absent on every record written before this key existed, and absent
|
||||
// again — rather than fatal — if a future build's stamp `Kind` is one this build cannot name.
|
||||
// A stamp that cannot be read is a stamp that cannot recover anything, which degrades to the
|
||||
// pause-and-defer stance rather than to a quarantined registry.
|
||||
gitOperationStamp = try? container.decodeIfPresent(GitOperationStamp.self, forKey: .gitOperationStamp)
|
||||
}
|
||||
|
||||
public func encode(to encoder: any Encoder) throws {
|
||||
@@ -251,6 +269,7 @@ public struct BoardRecord: Codable, Sendable, Equatable, Identifiable {
|
||||
try container.encodeIfPresent(iconColor, forKey: .iconColor)
|
||||
try container.encode(pushOnCommit, forKey: .pushOnCommit)
|
||||
try container.encode(remoteLocationWarned, forKey: .remoteLocationWarned)
|
||||
try container.encodeIfPresent(gitOperationStamp, forKey: .gitOperationStamp)
|
||||
// An unknown key is dropped, exactly as the synthesized conformance dropped it: the file's
|
||||
// forward tolerance is a decoding property, and nothing here preserves what it cannot read.
|
||||
}
|
||||
@@ -580,6 +599,22 @@ public final class BoardRegistry {
|
||||
update(id) { $0.remoteLocationWarned = true }
|
||||
}
|
||||
|
||||
/// **Records — or clears — the bracketed git operation this app is about to run**
|
||||
/// (`GitOperationStamp`).
|
||||
///
|
||||
/// It saves the file synchronously like every other setter here, and that is load-bearing rather
|
||||
/// than incidental: the whole value of the stamp is that it is on disk *before* the repository is
|
||||
/// touched, so a crash a millisecond later is still recognizable as this app's. `save()` writes
|
||||
/// atomically, so the file a next launch reads is either the old one or this one.
|
||||
public func setGitOperationStamp(id: UUID, _ stamp: GitOperationStamp?) {
|
||||
update(id) { $0.gitOperationStamp = stamp }
|
||||
}
|
||||
|
||||
/// The stamp this board is carrying, if any — read once per session, at open.
|
||||
public func gitOperationStamp(id: UUID) -> GitOperationStamp? {
|
||||
record(id: id)?.gitOperationStamp
|
||||
}
|
||||
|
||||
// MARK: - Reading
|
||||
|
||||
/// Every known board, most recently opened first, each classified by whether its bookmark
|
||||
|
||||
Reference in New Issue
Block a user