diff --git a/src/Server/RefreshScheduler.fs b/src/Server/RefreshScheduler.fs index b10fe05..8e29205 100644 --- a/src/Server/RefreshScheduler.fs +++ b/src/Server/RefreshScheduler.fs @@ -214,14 +214,16 @@ let resolveArchivedPaths (archivedBranchSets: Map>) (repos: |> Option.map (fun _ -> wt.Path)) |> Set.ofList) +let isWorktreeIgnored (ignorePredicate: string -> bool) (wt: GitWorktree.WorktreeInfo) = + (wt.Branch |> Option.exists ignorePredicate) + || (wt.Path |> Path.GetFileName |> ignorePredicate) + let resolveIgnoredPaths (ignorePredicate: string -> bool) (repos: Map) = repos |> Map.map (fun _ repo -> repo.WorktreeList - |> List.choose (fun wt -> - wt.Branch - |> Option.filter ignorePredicate - |> Option.map (fun _ -> wt.Path)) + |> List.filter (isWorktreeIgnored ignorePredicate) + |> List.map _.Path |> Set.ofList) type PathFilters = @@ -428,7 +430,7 @@ let runInitialBurst (agent: MailboxProcessor) (rootPaths: Map TreemonConfig.buildIgnorePredicate + let ignorePredicate = TreemonConfig.readIgnoreWorktreePatterns () |> TreemonConfig.buildIgnorePredicate let ignoredPaths = resolveIgnoredPaths ignorePredicate state.Repos let filters = { Archived = archivedPaths; Ignored = ignoredPaths } Log.log "Scheduler" "Starting initial burst — Phase 2 (local data + fetch)" @@ -488,7 +490,7 @@ let start (agent: MailboxProcessor) (worktreeRoots: string list) (ct: let archivedBranchSets = readArchivedBranchSets rootPaths let archivedPaths = resolveArchivedPaths archivedBranchSets repos - let ignorePredicate = TreemonConfig.readIgnoreBranchPatterns () |> TreemonConfig.buildIgnorePredicate + let ignorePredicate = TreemonConfig.readIgnoreWorktreePatterns () |> TreemonConfig.buildIgnorePredicate let ignoredPaths = resolveIgnoredPaths ignorePredicate repos let tasks = buildTaskList { Archived = archivedPaths; Ignored = ignoredPaths } repos let now = DateTimeOffset.UtcNow diff --git a/src/Server/TreemonConfig.fs b/src/Server/TreemonConfig.fs index da9e2c7..3edc640 100644 --- a/src/Server/TreemonConfig.fs +++ b/src/Server/TreemonConfig.fs @@ -111,9 +111,9 @@ let private withGlobalConfig (defaultValue: 'a) (f: JsonElement -> 'a) : 'a = Log.log "TreemonConfig" $"Failed to read global config: {ex.Message}" defaultValue -let readIgnoreBranchPatterns () : string list = +let readIgnoreWorktreePatterns () : string list = withGlobalConfig [] (fun root -> - match root.TryGetProperty("ignoreBranchPatterns") with + match root.TryGetProperty("ignoreWorktreePatterns") with | true, prop when prop.ValueKind = JsonValueKind.Array -> prop.EnumerateArray() |> Seq.choose (fun el -> @@ -129,8 +129,8 @@ let buildIgnorePredicate (patterns: string list) : string -> bool = |> List.choose (fun pattern -> try Some (Regex($"^(?:{pattern})$", RegexOptions.Compiled)) with :? ArgumentException -> - Log.log "TreemonConfig" $"Invalid ignore branch pattern: '{pattern}'" + Log.log "TreemonConfig" $"Invalid ignore worktree pattern: '{pattern}'" None) match regexes with | [] -> fun _ -> false - | _ -> fun branch -> regexes |> List.exists _.IsMatch(branch) + | _ -> fun value -> regexes |> List.exists _.IsMatch(value) diff --git a/src/Server/WorktreeApi.fs b/src/Server/WorktreeApi.fs index c2e03e0..fee08c6 100644 --- a/src/Server/WorktreeApi.fs +++ b/src/Server/WorktreeApi.fs @@ -160,12 +160,6 @@ let private readCollapsedRepos () : Set = |> Set.ofSeq | _ -> Set.empty) -let private readIgnoreBranchPatterns () : string list = - TreemonConfig.readIgnoreBranchPatterns () - -let internal buildIgnorePredicate (patterns: string list) : string -> bool = - TreemonConfig.buildIgnorePredicate patterns - let private writeCollapsedRepos (repos: RepoId list) = let configPath = globalConfigPath () try @@ -208,7 +202,7 @@ let getWorktrees let! activeSessions = SessionManager.getActiveSessions sessionAgent let activeSessionPaths = activeSessions |> Map.keys |> Set.ofSeq - let ignoreBranch = readIgnoreBranchPatterns () |> buildIgnorePredicate + let ignorePredicate = TreemonConfig.readIgnoreWorktreePatterns () |> TreemonConfig.buildIgnorePredicate let repos = state.Repos @@ -221,7 +215,7 @@ let getWorktrees let statuses = repo.WorktreeList - |> List.filter (fun wt -> wt.Branch |> Option.exists ignoreBranch |> not) + |> List.filter (RefreshScheduler.isWorktreeIgnored ignorePredicate >> not) |> List.map (fun wt -> let hasLog = SyncEngine.testFailureLogPath wt.Path |> System.IO.File.Exists assembleFromState activeSessionPaths archivedBranches hasLog repo wt) diff --git a/src/Tests/SchedulerTests.fs b/src/Tests/SchedulerTests.fs index bbc0346..cc4ca86 100644 --- a/src/Tests/SchedulerTests.fs +++ b/src/Tests/SchedulerTests.fs @@ -1049,18 +1049,45 @@ type ResolveIgnoredPathsTests() = Assert.That(ignored |> Set.contains "/r1/main", Is.False, "main should not be ignored") [] - member _.``Worktree with no branch is not ignored``() = + member _.``Matches worktrees by folder name``() = + let repos = + [ RepoId "Repo1", makeRepo [ makeWorktree "/r1/main" "main"; makeWorktree "/r1/archive-foo" "feature/abc" ] ] + |> Map.ofList + + let predicate = Server.TreemonConfig.buildIgnorePredicate [ "archive-.*" ] + let result = resolveIgnoredPaths predicate repos + + let ignored = result |> Map.find (RepoId "Repo1") + Assert.That(ignored |> Set.contains "/r1/archive-foo", Is.True, "archive-foo folder should be ignored") + Assert.That(ignored |> Set.contains "/r1/main", Is.False, "main should not be ignored") + + [] + member _.``Worktree with no branch is ignored when folder matches``() = + let repo = + { PerRepoState.empty with + WorktreeList = [ { Path = "/r1/detached"; Head = "abc123"; Branch = None } ] + KnownPaths = Set.ofList [ "/r1/detached" ] } + let repos = [ RepoId "Repo1", repo ] |> Map.ofList + + let predicate = Server.TreemonConfig.buildIgnorePredicate [ "detached" ] + let result = resolveIgnoredPaths predicate repos + + let ignored = result |> Map.find (RepoId "Repo1") + Assert.That(ignored |> Set.contains "/r1/detached", Is.True, "detached folder should be ignored") + + [] + member _.``Worktree with no branch is not ignored when folder does not match``() = let repo = { PerRepoState.empty with WorktreeList = [ { Path = "/r1/detached"; Head = "abc123"; Branch = None } ] KnownPaths = Set.ofList [ "/r1/detached" ] } let repos = [ RepoId "Repo1", repo ] |> Map.ofList - let predicate = Server.TreemonConfig.buildIgnorePredicate [ ".*" ] + let predicate = Server.TreemonConfig.buildIgnorePredicate [ "archive-.*" ] let result = resolveIgnoredPaths predicate repos let ignored = result |> Map.find (RepoId "Repo1") - Assert.That(ignored, Is.Empty, "Worktree with no branch should never be ignored") + Assert.That(ignored, Is.Empty, "Worktree with no branch should not be ignored when folder does not match") [] member _.``No patterns produces empty ignored sets``() = @@ -1087,7 +1114,7 @@ type BuildIgnorePredicateTests() = Assert.That(predicate "feature/abc", Is.False) [] - member _.``Regex pattern matches branch names``() = + member _.``Regex pattern matches values``() = let predicate = Server.TreemonConfig.buildIgnorePredicate [ "feature/.*"; "hotfix/.*" ] Assert.That(predicate "feature/abc", Is.True) Assert.That(predicate "hotfix/urgent", Is.True)