private/Get-WtwUntrackedWorktree.ps1
|
function Get-WtwUntrackedWorktree { <# .SYNOPSIS Git worktrees of registered repos that wtw does not track yet. .DESCRIPTION Other tools (T3 Code, Codex, Cursor, plain ``git worktree add``) create worktrees next to a repo wtw already tracks. They show up here so ``wtw list`` can display them and ``wtw go`` / ``wtw open`` can adopt them on first use. The primary checkout, bare entries, and trees whose folder is gone (prunable) are skipped. .OUTPUTS PSCustomObject with RepoName, RepoEntry, Path, Branch, Detached, TaskName. TaskName is the folder name with the ``<repo>_`` prefix stripped, which is the key ``wtw add`` would register it under. #> [CmdletBinding()] param( [Parameter(Mandatory)] $Registry, [string] $RepoName ) $items = [System.Collections.Generic.List[object]]::new() foreach ($name in (Get-WtwPropertyNames -Object $Registry.repos)) { if ($RepoName -and $name -ne $RepoName) { continue } $repo = $Registry.repos.$name $mainPath = Get-WtwPropertyValue -Object $repo -Name 'mainPath' if (-not $mainPath -or -not (Test-Path -LiteralPath $mainPath)) { continue } $listed = @(Get-WtwGitWorktreeList -RepoPath $mainPath) $worktrees = Get-WtwPropertyValue -Object $repo -Name 'worktrees' $registeredPaths = @(foreach ($task in (Get-WtwPropertyNames -Object $worktrees)) { Get-WtwPropertyValue -Object $worktrees.$task -Name 'path' }) Initialize-WtwRealPathCache -Paths (@($mainPath) + $registeredPaths + @($listed | ForEach-Object { $_.Path })) $registeredMap = Get-WtwRegisteredWorktreePathMap -RepoEntry $repo $index = 0 foreach ($wt in $listed) { $index++ if ($wt.Bare -or $wt.Prunable) { continue } if ($index -eq 1 -or (Test-WtwSamePath $wt.Path $mainPath)) { continue } if ($registeredMap.Contains($wt.Path)) { continue } if (-not (Test-Path -LiteralPath $wt.Path)) { continue } $canonical = Resolve-WtwRealPath $wt.Path if ($canonical -and $registeredMap.Contains($canonical)) { continue } $leaf = Split-Path $wt.Path -Leaf $taskName = $leaf -replace "^$([regex]::Escape($name))_", '' if (-not $taskName) { $taskName = $leaf } [void]$items.Add([pscustomobject]@{ RepoName = $name RepoEntry = $repo Path = $wt.Path Branch = $wt.Branch Detached = [bool]$wt.Detached TaskName = $taskName }) } } return @($items) } function Find-WtwUntrackedWorktree { <# .SYNOPSIS Untracked worktrees whose folder, task name, branch, or path match a query. .DESCRIPTION Tries exact, then prefix, then substring (case-insensitive) and returns the first non-empty tier, so an exact folder name beats a longer one that merely contains it. An absolute path matches that worktree's path. #> [CmdletBinding()] param( [Parameter(Mandatory)] $Registry, [Parameter(Mandatory)] [string] $Name, [string] $RepoName ) $untracked = @(Get-WtwUntrackedWorktree -Registry $Registry -RepoName $RepoName) if ($untracked.Count -eq 0) { return @() } if ([System.IO.Path]::IsPathRooted($Name)) { return @($untracked | Where-Object { Test-WtwSamePath $_.Path $Name }) } $needle = (ConvertTo-WtwLookupKey $Name) foreach ($mode in @('Exact', 'Prefix', 'Substring')) { $hits = @($untracked | Where-Object { $item = $_ $candidates = @( $item.TaskName (Split-Path $item.Path -Leaf) $item.Branch ) | Where-Object { $_ } | ForEach-Object { ConvertTo-WtwLookupKey $_ } $matched = $false foreach ($candidate in $candidates) { $matched = switch ($mode) { 'Exact' { $candidate -eq $needle } 'Prefix' { $candidate.StartsWith($needle) } 'Substring' { $candidate.Contains($needle) } } if ($matched) { break } } $matched }) if ($hits.Count -gt 0) { return $hits } } return @() } function Add-WtwUntrackedTarget { <# .SYNOPSIS Adopt one untracked worktree and return it in Resolve-WtwTarget's shape. .DESCRIPTION Runs ``wtw add`` (color, workspace file, editor registrations). The task key gets a numeric suffix when the folder name is already taken. #> [CmdletBinding()] param( [Parameter(Mandatory)] $Item, [string] $Name ) $existing = Get-WtwPropertyNames -Object $Item.RepoEntry.worktrees $task = $Item.TaskName $suffix = 2 while ($existing -contains $task) { $task = "$($Item.TaskName)-$suffix" $suffix++ } Write-WtwHost " '$Name' is an untracked worktree of $($Item.RepoName): $($Item.Path)" -ForegroundColor Cyan Write-WtwHost ' Adding it to wtw...' -ForegroundColor Cyan $null = Add-WtwEntry -Path $Item.Path -Repo $Item.RepoName -Task $task $repo = (Get-WtwRegistry).repos.($Item.RepoName) if (-not $repo -or ((Get-WtwPropertyNames -Object $repo.worktrees) -notcontains $task)) { return $null } return [PSCustomObject]@{ RepoName = $Item.RepoName RepoEntry = $repo TaskName = $task WorktreeEntry = $repo.worktrees.$task } } function Find-WtwGoneWorktree { <# .SYNOPSIS Registered worktrees git has already removed. .DESCRIPTION A registered worktree is gone when its folder no longer exists and git either no longer lists it or lists it as prunable. git is only asked for repos that have a missing folder, so the usual case costs one Test-Path per entry. A repo whose main checkout is missing, or where git cannot list worktrees, is skipped: there is nothing to compare against. .OUTPUTS PSCustomObject with RepoName, TaskName, Path, Key ("<repo>/<task>"). #> [CmdletBinding()] param( [Parameter(Mandatory)] $Registry ) $gone = [System.Collections.Generic.List[object]]::new() foreach ($repoName in (Get-WtwPropertyNames -Object $Registry.repos)) { $repo = $Registry.repos.$repoName $worktrees = Get-WtwPropertyValue -Object $repo -Name 'worktrees' $taskNames = Get-WtwPropertyNames -Object $worktrees if (-not $taskNames) { continue } $missing = @(foreach ($task in $taskNames) { $path = Get-WtwPropertyValue -Object $worktrees.$task -Name 'path' if ($path -and -not (Test-Path -LiteralPath $path)) { [pscustomobject]@{ RepoName = $repoName; TaskName = $task; Path = $path; Key = "$repoName/$task" } } }) if ($missing.Count -eq 0) { continue } $mainPath = Get-WtwPropertyValue -Object $repo -Name 'mainPath' if (-not $mainPath -or -not (Test-Path -LiteralPath $mainPath)) { continue } $listed = @(Get-WtwGitWorktreeList -RepoPath $mainPath) if ($listed.Count -eq 0) { continue } # A missing folder git still lists as live (an unmounted drive) stays. $live = @($listed | Where-Object { -not $_.Prunable } | ForEach-Object { $_.Path }) Initialize-WtwRealPathCache -Paths $live $liveKeys = @($live | ForEach-Object { ConvertTo-WtwPathKey $_ }) foreach ($item in $missing) { if ($liveKeys -notcontains (ConvertTo-WtwPathKey $item.Path)) { [void]$gone.Add($item) } } } return @($gone) } function Remove-WtwGoneWorktree { <# .SYNOPSIS Drop registry entries for worktrees git has already removed. .DESCRIPTION Removes each entry from Find-WtwGoneWorktree (or ``-Gone``) together with its color assignment and generated workspace file. .OUTPUTS One "<repo>/<task>" string per removed entry. #> [CmdletBinding()] param( [Parameter(Mandatory)] $Registry, [object[]] $Gone ) if (-not $PSBoundParameters.ContainsKey('Gone')) { $Gone = @(Find-WtwGoneWorktree -Registry $Registry) } $goneKeys = @($Gone | ForEach-Object { $_.Key }) if ($goneKeys.Count -eq 0) { return @() } $removed = [System.Collections.Generic.List[string]]::new() foreach ($repoName in @($Gone | ForEach-Object { $_.RepoName } | Select-Object -Unique)) { $repo = $Registry.repos.$repoName $kept = [PSCustomObject]@{} foreach ($prop in $repo.worktrees.PSObject.Properties) { if ("$repoName/$($prop.Name)" -in $goneKeys) { $workspace = Get-WtwPropertyValue -Object $prop.Value -Name 'workspace' if ($workspace -and (Test-Path -LiteralPath $workspace)) { Remove-Item -LiteralPath $workspace -Force -ErrorAction SilentlyContinue } [void]$removed.Add("$repoName/$($prop.Name)") } else { $kept | Add-Member -NotePropertyName $prop.Name -NotePropertyValue $prop.Value } } $repo.worktrees = $kept } if ($removed.Count -eq 0) { return @() } Save-WtwRegistry $Registry $colors = Get-WtwColors $assignments = [PSCustomObject]@{} foreach ($prop in $colors.assignments.PSObject.Properties) { if ($prop.Name -notin $removed) { $assignments | Add-Member -NotePropertyName $prop.Name -NotePropertyValue $prop.Value } } $colors.assignments = $assignments Save-WtwColors $colors return @($removed) } |