The pro-m1 foundation card. SwiftGitX 0.4.0 (bundled libgit2, the pathfinder's pin) joins the one target; new Kanban/Git/ holds BoardGitMode (pure nearest-.git-wins detection, .git-as-file counts, NSString ancestor walk), HistoryStore (@MainActor @Observable; compose() is the tier gate — free tier gets no object, no detection, no stat), GitRepository (scope-confined SwiftGitX handles: create = init + HEAD forced to main + whole-tree "Initial board state" commit; branch reads incl. unborn/detached; path-history ranks), GitIdentity (derived default as a pure function + repo-local config reader — not libgit2's merged ladder), and GitPathHistory (Mutex-guarded lazy ranker). beginSession composes the git state beside the tier and feeds BoardStore.makeIdentityHistoryRanker; git-mode loads pass the git-backed IdentityHistoryRanker to BoardLoader. The popover's git slot resolves a pure five-way matrix: free tier unchanged (absent / BoardGitNote), Pro mode-aware — Add Git on mode none, honest prose on repo-nested, read-only branch line on git. Provider binding unchanged: both tiers still bind native until the undo/redo card. 42 new tests across 8 suites, all repositories built through bundled libgit2; InertGitTests untouched and green. 2194 tests / 375 suites. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
130 lines
5.4 KiB
Swift
130 lines
5.4 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import Kanban
|
|
|
|
/// **Nearest-`.git`-wins, freshly at every open** (06-history-undo.md ▸ Rules ▸ Detection) — the one
|
|
/// question every git surface starts from, pinned against real directories rather than against a
|
|
/// mocked filesystem, because what it is *about* is what is on disk.
|
|
///
|
|
/// The rule has four claims and this file is one test per claim: root wins, an ancestor is nested,
|
|
/// neither is `none`, and the answer is re-derived rather than remembered — "a board can therefore
|
|
/// change mode between opens (e.g. the user ran `git init` in a terminal) — the app just reflects
|
|
/// what it finds."
|
|
|
|
// MARK: - Fixtures
|
|
|
|
/// A `.git` **directory** with a plausible ref inside — what `git init` leaves.
|
|
private func makeGitDirectory(at parent: URL) throws {
|
|
let gitDirectory = parent.appendingPathComponent(".git", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: gitDirectory, withIntermediateDirectories: true)
|
|
try Data("ref: refs/heads/main\n".utf8).write(to: gitDirectory.appendingPathComponent("HEAD"))
|
|
}
|
|
|
|
/// A `.git` **file** — what a linked worktree or a submodule leaves. Still a repository, and the
|
|
/// reason detection asks `fileExists` rather than `isDirectory`.
|
|
private func makeGitPointerFile(at parent: URL, to target: String) throws {
|
|
try Data("gitdir: \(target)\n".utf8).write(to: parent.appendingPathComponent(".git"))
|
|
}
|
|
|
|
/// A board folder inside the fixture, so an *ancestor* can carry the repository.
|
|
private func makeSubfolder(_ fixture: WriterFixture, named name: String) throws -> URL {
|
|
let url = fixture.root.appendingPathComponent(name, isDirectory: true)
|
|
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
|
|
return url
|
|
}
|
|
|
|
// MARK: - Detection
|
|
|
|
@Suite("Board git mode ▸ detection")
|
|
struct BoardGitModeTests {
|
|
|
|
@Test("A `.git` at the board root is git mode")
|
|
func rootRepositoryIsGitMode() throws {
|
|
let fixture = try WriterFixture()
|
|
defer { fixture.tearDown() }
|
|
try fixture.item("", Item.board)
|
|
try makeGitDirectory(at: fixture.root)
|
|
|
|
#expect(BoardGitMode.detect(boardRoot: fixture.root) == .git)
|
|
}
|
|
|
|
@Test("A `.git` *file* is a repository too — a worktree is not mode none")
|
|
func aWorktreePointerIsGitMode() throws {
|
|
let fixture = try WriterFixture()
|
|
defer { fixture.tearDown() }
|
|
try fixture.item("", Item.board)
|
|
try makeGitPointerFile(at: fixture.root, to: "/somewhere/else/.git/worktrees/board")
|
|
|
|
#expect(BoardGitMode.detect(boardRoot: fixture.root) == .git)
|
|
}
|
|
|
|
@Test("No `.git` at the root and none above it is mode none")
|
|
func aPlainFolderIsModeNone() throws {
|
|
let fixture = try WriterFixture()
|
|
defer { fixture.tearDown() }
|
|
try fixture.item("", Item.board)
|
|
|
|
#expect(BoardGitMode.detect(boardRoot: fixture.root) == .none)
|
|
}
|
|
|
|
@Test("A `.git` at an ancestor and none at the root is repo-nested")
|
|
func anEnclosingRepositoryIsRepoNested() throws {
|
|
let fixture = try WriterFixture()
|
|
defer { fixture.tearDown() }
|
|
try makeGitDirectory(at: fixture.root)
|
|
let board = try makeSubfolder(fixture, named: "project/docs/board")
|
|
|
|
#expect(BoardGitMode.detect(boardRoot: board) == .repoNested)
|
|
#expect(BoardGitMode.enclosingRepositoryRoot(above: board)?.standardizedFileURL
|
|
== fixture.root.standardizedFileURL)
|
|
}
|
|
|
|
@Test("Nearest wins: a board with its own `.git` inside a repository is git mode, not nested")
|
|
func theNearestRepositoryWins() throws {
|
|
let fixture = try WriterFixture()
|
|
defer { fixture.tearDown() }
|
|
try makeGitDirectory(at: fixture.root)
|
|
let board = try makeSubfolder(fixture, named: "board")
|
|
try makeGitDirectory(at: board)
|
|
|
|
#expect(BoardGitMode.detect(boardRoot: board) == .git)
|
|
}
|
|
|
|
@Test("The walk terminates above a board at the filesystem root, finding nothing")
|
|
func theAncestorWalkTerminates() {
|
|
// The one hazard this walk has ever had: NSURL-bridged URLs whose
|
|
// `deletingLastPathComponent` grows "/.." forever instead of settling at "/". The temp
|
|
// directory has no repository above it, so the honest answer is `nil` — reached, not hung.
|
|
#expect(BoardGitMode.enclosingRepositoryRoot(above: URL(fileURLWithPath: "/")) == nil)
|
|
}
|
|
|
|
// MARK: Freshness
|
|
|
|
@Test("Detection is re-derived at every open: a board that gains a `.git` opens in git mode next time")
|
|
func modeChangesBetweenOpens() throws {
|
|
let fixture = try WriterFixture()
|
|
defer { fixture.tearDown() }
|
|
try fixture.item("", Item.board)
|
|
|
|
#expect(BoardGitMode.detect(boardRoot: fixture.root) == .none, "the first open")
|
|
|
|
// What a user does in a terminal under a closed board.
|
|
try makeGitDirectory(at: fixture.root)
|
|
|
|
#expect(BoardGitMode.detect(boardRoot: fixture.root) == .git, "the next open reflects what it finds")
|
|
}
|
|
|
|
@Test("And a board that loses its `.git` opens back in mode none")
|
|
func modeChangesBackBetweenOpens() throws {
|
|
let fixture = try WriterFixture()
|
|
defer { fixture.tearDown() }
|
|
try fixture.item("", Item.board)
|
|
try makeGitDirectory(at: fixture.root)
|
|
#expect(BoardGitMode.detect(boardRoot: fixture.root) == .git)
|
|
|
|
try FileManager.default.removeItem(at: fixture.root.appendingPathComponent(".git"))
|
|
|
|
#expect(BoardGitMode.detect(boardRoot: fixture.root) == .none)
|
|
}
|
|
}
|