private/Import-WtwEnvironment.ps1

function ConvertTo-WtwCanonicalGitUrl {
    <#
    .SYNOPSIS
        Identity of a git remote, so two clones of one repo compare equal.
    .DESCRIPTION
        `git@host:org/repo.git`, `https://host/org/repo`, and `ssh://git@host/org/repo.git`
        collapse to `host/org/repo`. Local paths (a bare repo used as origin) collapse
        to a symlink-resolved filesystem path.
    #>

    [CmdletBinding()]
    param([AllowNull()] [string] $Url)

    if ([string]::IsNullOrWhiteSpace($Url)) { return $null }
    $text = $Url.Trim().TrimEnd('/', '\')

    # scp-style: git@github.com:org/repo.git (no scheme)
    if ($text -notmatch '://' -and $text -match '^[^@\s]+@(?<host>[^:]+):(?<path>.+)$') {
        return (Format-WtwGitHostPath -GitHost $Matches.host -GitPath $Matches.path)
    }

    if ($text -match '^[a-zA-Z][a-zA-Z0-9+.-]*://') {
        $asUri = $text
        if ($asUri -match '^file://(?<path>/.*)$') {
            return (ConvertTo-WtwCanonicalLocalGitPath -Path $Matches.path)
        }
        try {
            $uri = [Uri]$asUri
            if ($uri.Host) {
                return (Format-WtwGitHostPath -GitHost $uri.Host -GitPath $uri.AbsolutePath)
            }
        } catch {
            Write-Verbose "git url '$text' is not a URI: $_"
        }
    }

    return (ConvertTo-WtwCanonicalLocalGitPath -Path $text)
}

function Format-WtwGitHostPath {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $GitHost,
        [Parameter(Mandatory)] [string] $GitPath
    )

    $hostName = $GitHost.ToLowerInvariant()
    $path = $GitPath.Trim().Trim('/').TrimEnd('/')
    $path = $path -replace '\.git$', ''
    $path = $path.ToLowerInvariant()
    if (-not $path) { return $hostName }
    return "$hostName/$path"
}

function ConvertTo-WtwCanonicalLocalGitPath {
    [CmdletBinding()]
    param([Parameter(Mandatory)] [string] $Path)

    $candidate = $Path -replace '^file://', ''
    $candidate = $candidate -replace '\\', '/'
    if (Test-Path -LiteralPath $candidate) {
        $real = Resolve-WtwRealPath -Path $candidate
        if ($real) { return $real.TrimEnd('/', '\') }
    }
    return $candidate.TrimEnd('/', '\')
}

function Get-WtwGitFetchRemotes {
    <#
    .SYNOPSIS
        Fetch remotes of a checkout: name + url.
    #>

    [CmdletBinding()]
    param([Parameter(Mandatory)] [string] $RepoPath)

    $found = @()
    if (-not (Test-Path -LiteralPath $RepoPath)) { return , @() }
    foreach ($line in @(git -C $RepoPath remote -v 2>$null)) {
        $text = "$line"
        if ($text -match '^(?<name>\S+)\s+(?<url>\S+)\s+\(fetch\)$') {
            $remoteName = $Matches.name
            $remoteUrl = $Matches.url
            $found += [PSCustomObject]@{ name = $remoteName; url = $remoteUrl }
        }
    }
    return , @($found)
}

function Get-WtwWorktreeFolderSuffix {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $RepoName,
        [Parameter(Mandatory)] [string] $TaskName,
        [AllowNull()] [string] $WorktreePath
    )

    if ($WorktreePath) {
        $leaf = Split-Path -Path $WorktreePath -Leaf
        $prefix = "${RepoName}_"
        if ($leaf.StartsWith($prefix, [System.StringComparison]::Ordinal) -and $leaf.Length -gt $prefix.Length) {
            return $leaf.Substring($prefix.Length)
        }
    }
    return $TaskName
}

function New-WtwWorktreeExportFromTarget {
    <#
    .SYNOPSIS
        Snapshot another machine needs in order to recreate this worktree.
    #>

    [CmdletBinding()]
    param([Parameter(Mandatory)] $Target)

    $entry = $Target.WorktreeEntry
    $path = [string](Get-WtwPropertyValue -Object $entry -Name 'path')
    $main = [string](Get-WtwPropertyValue -Object $Target.RepoEntry -Name 'mainPath')
    if (-not $main) { $main = $path }

    $branchText = "$(@(git -C $path rev-parse --abbrev-ref HEAD 2>$null) | Select-Object -First 1)".Trim()
    $detached = (-not $branchText -or $branchText -eq 'HEAD')
    $branch = if ($detached) { $null } else { $branchText }
    $commit = "$(@(git -C $path rev-parse HEAD 2>$null) | Select-Object -First 1)".Trim()
    if (-not $commit) {
        Write-Error "Could not read HEAD of $path."
        return $null
    }

    $dirtyLines = @(git -C $path status --porcelain 2>$null | Where-Object { "$_".Trim() })
    # These helpers `return , @()`. Wrapping that single array in @() nests it,
    # and strict mode then cannot read .url / the alias strings.
    $aliases = Get-WtwWorktreeAliases -Worktree $entry
    if ($null -eq $aliases) { $aliases = @() }
    $repoAliases = @()
    $repoProps = @(Get-WtwPropertyNames -Object $Target.RepoEntry)
    if ($repoProps -contains 'aliases' -or $repoProps -contains 'alias') {
        $repoAliases = Get-WtwRepoAliases -Repo $Target.RepoEntry
        if ($null -eq $repoAliases) { $repoAliases = @() }
    }
    $remotes = Get-WtwGitFetchRemotes -RepoPath $main
    if ($null -eq $remotes) { $remotes = @() }

    return [PSCustomObject]@{
        kind         = 'wtw-export'
        version      = 1
        error        = $null
        repo         = $Target.RepoName
        repoAliases  = $repoAliases
        repoEmoji    = (Get-WtwRepoEmoji -RepoEntry $Target.RepoEntry)
        remotes      = $remotes
        task         = $Target.TaskName
        branch       = $branch
        commit       = $commit
        detached     = [bool]$detached
        folderSuffix = (Get-WtwWorktreeFolderSuffix -RepoName $Target.RepoName -TaskName $Target.TaskName -WorktreePath $path)
        color        = Get-WtwPropertyValue -Object $entry -Name 'color'
        prettyName   = Get-WtwPropertyValue -Object $entry -Name 'prettyName'
        emoji        = Get-WtwPropertyValue -Object $entry -Name 'emoji'
        aliases      = $aliases
        dirty        = ($dirtyLines.Count -gt 0)
    }
}

function Get-WtwWorktreeExport {
    <#
    .SYNOPSIS
        Resolve a local worktree the way ``wtw go`` does, and describe it for import.
    .DESCRIPTION
        ``wtw __export_json`` prints this. A name that resolves to the main checkout
        returns an export with error=main so the other machine can say so, instead
        of creating a second checkout of the default branch.
    #>

    [CmdletBinding()]
    param([Parameter(Mandatory)] [string] $Name)

    $target = & { Resolve-WtwTarget -Name $Name } 6>$null
    if (-not $target) { return $null }
    if (-not $target.TaskName -or -not $target.WorktreeEntry) {
        return [PSCustomObject]@{
            kind  = 'wtw-export'
            error = 'main'
            repo  = $target.RepoName
        }
    }
    return (New-WtwWorktreeExportFromTarget -Target $target)
}

function ConvertFrom-WtwWorktreeExportText {
    <#
    .SYNOPSIS
        Parse a ``wtw-export`` JSON document out of remote stdout.
    #>

    [CmdletBinding()]
    param([AllowNull()] [string] $Text)

    if ([string]::IsNullOrWhiteSpace($Text)) { return $null }
    $start = $Text.IndexOf('{')
    $end = $Text.LastIndexOf('}')
    if ($start -lt 0 -or $end -le $start) { return $null }
    $json = $Text.Substring($start, ($end - $start + 1))
    try {
        $obj = $json | ConvertFrom-Json
    } catch {
        return $null
    }
    if ((Get-WtwPropertyValue -Object $obj -Name 'kind') -ne 'wtw-export') { return $null }
    return $obj
}

function New-WtwRemoteExportScript {
    <#
    .SYNOPSIS
        PowerShell run on a machine whose wtw predates ``__export_json``.
    .DESCRIPTION
        Prefers Get-WtwWorktreeExport when that function exists in the remote
        module. Otherwise collects the same fields with Resolve-WtwTarget and git,
        which gallery builds already have.
    #>

    [CmdletBinding()]
    param([Parameter(Mandatory)] [string] $Name)

    $literal = "'" + $Name.Replace("'", "''") + "'"
    $head = @'
$ErrorActionPreference = 'Continue'
$env:WTW_NO_UPDATE_NOTICE = '1'
if ($PSStyle) { $PSStyle.OutputRendering = 'PlainText' }
$wtwModule = Join-Path $HOME '.wtw' 'module' 'wtw.psm1'
if (Test-Path -LiteralPath $wtwModule) {
    Import-Module $wtwModule -Force -DisableNameChecking
} else {
    Import-Module wtw -DisableNameChecking
}
$module = Get-Module wtw
if (-not $module) { Write-Error 'wtw is not installed on this machine.'; exit 1 }
$json = & $module {
    param($ExportName)
    $exporter = Get-Command -Name Get-WtwWorktreeExport -ErrorAction SilentlyContinue
    if ($exporter) {
        $export = Get-WtwWorktreeExport -Name $ExportName
        if (-not $export) { return $null }
        return ($export | ConvertTo-Json -Compress -Depth 8)
    }
    $target = Resolve-WtwTarget -Name $ExportName
    if (-not $target) { return $null }
    if (-not $target.TaskName -or -not $target.WorktreeEntry) {
        return ([PSCustomObject]@{ kind = 'wtw-export'; error = 'main'; repo = $target.RepoName } | ConvertTo-Json -Compress)
    }
    $entry = $target.WorktreeEntry
    $path = [string]$entry.path
    $main = [string]$target.RepoEntry.mainPath
    if (-not $main) { $main = $path }
    $branchText = "$(@(git -C $path rev-parse --abbrev-ref HEAD 2>$null) | Select-Object -First 1)".Trim()
    $detached = (-not $branchText -or $branchText -eq 'HEAD')
    $branch = if ($detached) { $null } else { $branchText }
    $commit = "$(@(git -C $path rev-parse HEAD 2>$null) | Select-Object -First 1)".Trim()
    $dirty = @(git -C $path status --porcelain 2>$null | Where-Object { "$_".Trim() }).Count -gt 0
    $remotes = @()
    foreach ($line in @(git -C $main remote -v 2>$null)) {
        if ("$line" -match '^(?<name>\S+)\s+(?<url>\S+)\s+\(fetch\)$') {
            $remotes += [PSCustomObject]@{ name = $Matches.name; url = $Matches.url }
        }
    }
    $leaf = Split-Path -Path $path -Leaf
    $prefix = "$($target.RepoName)_"
    $suffix = [string]$target.TaskName
    if ($leaf.StartsWith($prefix) -and $leaf.Length -gt $prefix.Length) { $suffix = $leaf.Substring($prefix.Length) }
    $aliases = @()
    if ($entry.PSObject.Properties['aliases'] -and $entry.aliases) { $aliases = @($entry.aliases) }
    $emoji = if ($entry.PSObject.Properties['emoji']) { $entry.emoji } else { $null }
    $color = if ($entry.PSObject.Properties['color']) { $entry.color } else { $null }
    $pretty = if ($entry.PSObject.Properties['prettyName']) { $entry.prettyName } else { $null }
    [PSCustomObject]@{
        kind = 'wtw-export'; version = 1; error = $null
        repo = $target.RepoName; task = $target.TaskName
        branch = $branch; commit = $commit; detached = [bool]$detached
        folderSuffix = $suffix; color = $color; prettyName = $pretty
        emoji = $emoji; aliases = @($aliases); dirty = [bool]$dirty
        remotes = @($remotes)
    } | ConvertTo-Json -Compress -Depth 8
}
'@

    $tail = @'

if (-not $json) { exit 1 }
Write-Output $json
'@

    return ($head + $literal + $tail)
}

function Get-WtwRemoteWorktreeExport {
    <#
    .SYNOPSIS
        Ask another machine for the worktree snapshot ``import`` applies locally.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] $HostEntry,
        [Parameter(Mandatory)] [string] $Name
    )

    $primary = Invoke-WtwRemoteCommand -HostEntry $HostEntry -Arguments @('__export_json', $Name)
    if ($primary.Error -and (Test-WtwIsSshTransportError -ErrorText $primary.Error)) {
        Write-WtwSshFailure -HostEntry $HostEntry -ErrorText $primary.Error
        return $null
    }

    $parsed = ConvertFrom-WtwWorktreeExportText -Text (($primary.Output -join "`n"))
    if (-not $parsed) {
        # Gallery wtw without __export_json treats that token as ``wtw go``.
        $script = New-WtwRemoteExportScript -Name $Name
        $fallback = Invoke-WtwRemoteCommand -HostEntry $HostEntry -Script $script
        if ($fallback.Error -and (Test-WtwIsSshTransportError -ErrorText $fallback.Error)) {
            Write-WtwSshFailure -HostEntry $HostEntry -ErrorText $fallback.Error
            return $null
        }
        $parsed = ConvertFrom-WtwWorktreeExportText -Text (($fallback.Output -join "`n"))
        if (-not $parsed) {
            Show-WtwRemoteTargetSuggestions -HostEntry $HostEntry -Name $Name
            Write-Error "'$Name' did not resolve to a worktree on $($HostEntry.Name)."
            return $null
        }
    }

    if ((Get-WtwPropertyValue -Object $parsed -Name 'error') -eq 'main') {
        $repo = Get-WtwPropertyValue -Object $parsed -Name 'repo'
        Write-Error "'$Name' is the main checkout of '$repo' on $($HostEntry.Name). Import copies a worktree."
        return $null
    }
    return $parsed
}

function Find-WtwLocalRepoByGitRemotes {
    <#
    .SYNOPSIS
        Local registered repos that share a fetch URL with the remote snapshot.
        Always an array: empty, one, or several.
    #>

    [CmdletBinding()]
    param([AllowNull()] [object[]] $Remotes)

    $wanted = @{}
    foreach ($remote in $Remotes) {
        $url = Get-WtwPropertyValue -Object $remote -Name 'url'
        $canon = ConvertTo-WtwCanonicalGitUrl -Url $url
        if ($canon) { $wanted[$canon] = $true }
    }
    if ($wanted.Count -eq 0) { return , @() }

    $registry = Get-WtwRegistry
    # Not $matches — that name is the automatic regex variable, and -match
    # inside Get-WtwGitFetchRemotes would wipe the list we are building.
    $found = @()
    foreach ($repoName in (Get-WtwPropertyNames -Object $registry.repos)) {
        $entry = $registry.repos.$repoName
        $main = [string](Get-WtwPropertyValue -Object $entry -Name 'mainPath')
        if (-not $main -or -not (Test-Path -LiteralPath $main)) { continue }
        foreach ($localRemote in (Get-WtwGitFetchRemotes -RepoPath $main)) {
            $canon = ConvertTo-WtwCanonicalGitUrl -Url (Get-WtwPropertyValue -Object $localRemote -Name 'url')
            if ($canon -and $wanted.ContainsKey($canon)) {
                $found += [PSCustomObject]@{
                    RepoName    = $repoName
                    RepoEntry   = $entry
                    LocalRemote = [string](Get-WtwPropertyValue -Object $localRemote -Name 'name')
                }
                break
            }
        }
    }

    return , @($found)
}

function Find-WtwBranchCheckout {
    <#
    .SYNOPSIS
        Path of the worktree that already has this branch checked out, or $null.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $RepoPath,
        [Parameter(Mandatory)] [string] $Branch
    )

    $currentPath = $null
    foreach ($line in @(git -C $RepoPath worktree list --porcelain 2>$null)) {
        $text = "$line"
        if ($text -match '^worktree (.+)$') {
            $currentPath = $Matches[1].Trim()
            continue
        }
        if ($text -eq "branch refs/heads/$Branch") {
            return $currentPath
        }
    }
    return $null
}

function Test-WtwCommitPresent {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $RepoPath,
        [Parameter(Mandatory)] [string] $Commit
    )

    git -C $RepoPath cat-file -e "$Commit^{commit}" 2>$null
    return ($LASTEXITCODE -eq 0)
}

function Get-WtwImportBranchPlan {
    <#
    .SYNOPSIS
        How to attach a local branch to the commit the other machine is on.
    .DESCRIPTION
        Does not create anything. ``missing-commit`` means the object is not in
        this clone yet (import will fetch). ``ahead`` and ``diverged`` are refusals:
        a local branch that is not checked out still may hold commits the other
        machine does not have.
    .OUTPUTS
        @{ Action = create|adopt|fast-forward|ahead|diverged|detach|missing-commit }
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $RepoPath,
        [AllowNull()] [string] $Branch,
        [Parameter(Mandatory)] [string] $Commit,
        [bool] $Detached
    )

    if ($Detached -or [string]::IsNullOrWhiteSpace($Branch)) {
        if (-not (Test-WtwCommitPresent -RepoPath $RepoPath -Commit $Commit)) {
            return [PSCustomObject]@{ Action = 'missing-commit'; LocalCommit = $null }
        }
        return [PSCustomObject]@{ Action = 'detach'; LocalCommit = $null }
    }

    git -C $RepoPath show-ref --verify --quiet "refs/heads/$Branch" 2>$null
    $branchExists = ($LASTEXITCODE -eq 0)
    if (-not $branchExists) {
        if (-not (Test-WtwCommitPresent -RepoPath $RepoPath -Commit $Commit)) {
            return [PSCustomObject]@{ Action = 'missing-commit'; LocalCommit = $null }
        }
        return [PSCustomObject]@{ Action = 'create'; LocalCommit = $null }
    }

    $localCommit = "$(@(git -C $RepoPath rev-parse "refs/heads/$Branch" 2>$null) | Select-Object -First 1)".Trim()
    if (-not (Test-WtwCommitPresent -RepoPath $RepoPath -Commit $Commit)) {
        return [PSCustomObject]@{ Action = 'missing-commit'; LocalCommit = $localCommit }
    }
    if ($localCommit -eq $Commit) {
        return [PSCustomObject]@{ Action = 'adopt'; LocalCommit = $localCommit }
    }

    git -C $RepoPath merge-base --is-ancestor $localCommit $Commit 2>$null
    if ($LASTEXITCODE -eq 0) {
        return [PSCustomObject]@{ Action = 'fast-forward'; LocalCommit = $localCommit }
    }
    git -C $RepoPath merge-base --is-ancestor $Commit $localCommit 2>$null
    if ($LASTEXITCODE -eq 0) {
        return [PSCustomObject]@{ Action = 'ahead'; LocalCommit = $localCommit }
    }
    return [PSCustomObject]@{ Action = 'diverged'; LocalCommit = $localCommit }
}

function Sync-WtwImportCommit {
    <#
    .SYNOPSIS
        Fetch until the other machine's commit is in this clone, or give up.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $RepoPath,
        [Parameter(Mandatory)] [string] $RemoteName,
        [AllowNull()] [string] $Branch,
        [Parameter(Mandatory)] [string] $Commit,
        [bool] $Detached
    )

    if (Test-WtwCommitPresent -RepoPath $RepoPath -Commit $Commit) { return $true }

    Write-WtwHost " Fetching from $RemoteName..." -ForegroundColor Cyan
    if ($Branch -and -not $Detached) {
        git -C $RepoPath fetch $RemoteName "refs/heads/${Branch}:refs/remotes/${RemoteName}/${Branch}" 2>&1 | Out-Null
    }
    if (-not (Test-WtwCommitPresent -RepoPath $RepoPath -Commit $Commit)) {
        git -C $RepoPath fetch $RemoteName $Commit 2>&1 | Out-Null
    }
    if (-not (Test-WtwCommitPresent -RepoPath $RepoPath -Commit $Commit)) {
        git -C $RepoPath fetch $RemoteName 2>&1 | Out-Null
    }
    return (Test-WtwCommitPresent -RepoPath $RepoPath -Commit $Commit)
}

function Set-WtwImportUpstream {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $WorktreePath,
        [Parameter(Mandatory)] [string] $RemoteName,
        [Parameter(Mandatory)] [string] $Branch
    )

    git -C $WorktreePath rev-parse --verify --quiet "refs/remotes/$RemoteName/$Branch" 2>$null
    if ($LASTEXITCODE -ne 0) { return }
    git -C $WorktreePath branch --set-upstream-to "$RemoteName/$Branch" 2>$null | Out-Null
}

function Get-WtwShortCommit {
    param([AllowNull()] [string] $Commit)
    if (-not $Commit) { return '' }
    if ($Commit.Length -le 12) { return $Commit }
    return $Commit.Substring(0, 12)
}

function Import-WtwWorktreeSnapshot {
    <#
    .SYNOPSIS
        Create a local worktree from a remote export snapshot.
    .DESCRIPTION
        Matches the local repo by git remote URL, refuses when the branch is
        already checked out, then checks out the same branch and commit and
        registers color, emoji, pretty name, and aliases.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] $Snapshot,
        [string] $SourceName = 'remote',
        [switch] $DryRun
    )

    $task = [string](Get-WtwPropertyValue -Object $Snapshot -Name 'task')
    $branch = Get-WtwPropertyValue -Object $Snapshot -Name 'branch'
    $branch = if ($null -eq $branch) { '' } else { [string]$branch }
    $commit = [string](Get-WtwPropertyValue -Object $Snapshot -Name 'commit')
    $detached = [bool](Get-WtwPropertyValue -Object $Snapshot -Name 'detached' -DefaultValue $false)
    $suffix = [string](Get-WtwPropertyValue -Object $Snapshot -Name 'folderSuffix')
    if (-not $suffix) { $suffix = $task }

    if (-not $task -or -not $commit) {
        Write-Error "The snapshot from $SourceName is missing a task name or commit."
        return
    }

    $remotes = @(Get-WtwPropertyValue -Object $Snapshot -Name 'remotes')
    # Find returns `, @()`. @() around that would nest the match list.
    $localMatches = Find-WtwLocalRepoByGitRemotes -Remotes $remotes
    if ($null -eq $localMatches) { $localMatches = @() }
    if ($localMatches.Count -gt 1) {
        $names = ($localMatches | ForEach-Object { $_.RepoName }) -join ', '
        Write-Error "More than one local repo shares that git remote: $names. Unregister the extra checkout so import can tell which one you mean."
        return
    }
    if ($localMatches.Count -eq 0) {
        $shown = (@($remotes | ForEach-Object { Get-WtwPropertyValue -Object $_ -Name 'url' }) | Select-Object -First 3) -join ', '
        Write-Error "No local repo shares a git remote with '$task' on $SourceName ($shown). Clone it and run 'wtw init' in that checkout first."
        return
    }
    $local = $localMatches[0]

    $repoName = [string](Get-WtwPropertyValue -Object $local -Name 'RepoName')
    $repoEntry = $local.RepoEntry
    $mainPath = [string](Get-WtwPropertyValue -Object $repoEntry -Name 'mainPath')
    $parent = [string](Get-WtwPropertyValue -Object $repoEntry -Name 'worktreeParent')
    if (-not $parent) { $parent = Split-Path -Path $mainPath -Parent }

    $existingTasks = Get-WtwPropertyValue -Object $repoEntry -Name 'worktrees'
    if ($existingTasks -and (Get-WtwPropertyNames -Object $existingTasks) -contains $task) {
        Write-Error "Worktree '$task' is already registered under $repoName. Use 'wtw go $task'."
        return
    }

    if (-not $detached -and $branch) {
        $existingPath = Find-WtwBranchCheckout -RepoPath $mainPath -Branch $branch
        if ($existingPath) {
            Write-Error "Branch '$branch' is already checked out at $existingPath. Import leaves an existing checkout alone."
            return
        }
    }

    $worktreePath = Join-Path $parent "${repoName}_${suffix}"
    if (Test-Path -LiteralPath $worktreePath) {
        Write-Error "Path already exists: $worktreePath"
        return
    }

    $short = Get-WtwShortCommit -Commit $commit
    $plan = Get-WtwImportBranchPlan -RepoPath $mainPath -Branch $branch -Commit $commit -Detached:$detached
    $branchLabel = if ($detached -or -not $branch) { "(detached $short)" } else { $branch }

    Write-WtwHost ''
    Write-WtwHost " Import '$task' from $SourceName" -ForegroundColor Cyan
    Write-WtwHost " Repo: $repoName" -ForegroundColor Green
    Write-WtwHost " Branch: $branchLabel" -ForegroundColor Green
    Write-WtwHost " Commit: $short" -ForegroundColor Green
    Write-WtwHost " Path: $worktreePath" -ForegroundColor Green

    if ($plan.Action -eq 'ahead') {
        Write-Error "Local branch '$branch' in $repoName is ahead of $short on $SourceName. Import will not move it backwards."
        return
    }
    if ($plan.Action -eq 'diverged') {
        Write-Error "Local branch '$branch' in $repoName has diverged from $short on $SourceName. Import will not overwrite it."
        return
    }

    $dirty = [bool](Get-WtwPropertyValue -Object $Snapshot -Name 'dirty' -DefaultValue $false)
    if ($dirty) {
        Write-WtwHost " The worktree on $SourceName has uncommitted changes. Those files stay there." -ForegroundColor Yellow
    }

    if ($DryRun) {
        $verb = switch ($plan.Action) {
            'missing-commit' { "Would fetch $($local.LocalRemote) and check out $branchLabel." }
            'fast-forward'   { "Would fast-forward '$branch' to $short and add the worktree." }
            'create'         { "Would create '$branch' at $short." }
            'detach'         { "Would add a detached worktree at $short." }
            default          { "Would add a worktree on existing '$branch'." }
        }
        Write-WtwHost " Dry run: $verb" -ForegroundColor DarkCyan
        Write-WtwHost ''
        return
    }

    if ($plan.Action -eq 'missing-commit') {
        $fetched = Sync-WtwImportCommit -RepoPath $mainPath -RemoteName $local.LocalRemote -Branch $branch -Commit $commit -Detached:$detached
        if (-not $fetched) {
            $pushHint = if ($branch) { "Push '$branch' from $SourceName, then run import again." } else { "Push that commit from $SourceName, then run import again." }
            Write-Error "Commit $short is not in $repoName. $pushHint"
            return
        }
        $plan = Get-WtwImportBranchPlan -RepoPath $mainPath -Branch $branch -Commit $commit -Detached:$detached
        if ($plan.Action -in @('ahead', 'diverged', 'missing-commit')) {
            Write-Error "After fetch, '$branch' in $repoName still cannot move to $short ($($plan.Action))."
            return
        }
    }

    Write-WtwHost ' Creating worktree...' -ForegroundColor Cyan
    $gitOut = $null
    if ($plan.Action -eq 'detach') {
        $gitOut = git -C $mainPath worktree add --detach $worktreePath $commit 2>&1
    } elseif ($plan.Action -eq 'create') {
        $gitOut = git -C $mainPath worktree add -b $branch $worktreePath $commit 2>&1
    } else {
        $gitOut = git -C $mainPath worktree add $worktreePath $branch 2>&1
    }
    if ($LASTEXITCODE -ne 0) {
        Write-Error "git worktree add failed: $gitOut"
        return
    }

    if ($plan.Action -eq 'fast-forward') {
        $mergeOut = git -C $worktreePath merge --ff-only $commit 2>&1
        if ($LASTEXITCODE -ne 0) {
            git -C $mainPath worktree remove $worktreePath --force 2>$null | Out-Null
            Write-Error "Could not fast-forward '$branch' to ${short}: $mergeOut"
            return
        }
    }

    if (-not $detached -and $branch) {
        Set-WtwImportUpstream -WorktreePath $worktreePath -RemoteName $local.LocalRemote -Branch $branch
    }

    $pretty = Get-WtwNameWithoutColorCircle -Name ([string](Get-WtwPropertyValue -Object $Snapshot -Name 'prettyName'))
    $color = [string](Get-WtwPropertyValue -Object $Snapshot -Name 'color')
    $emoji = Get-WtwPropertyValue -Object $Snapshot -Name 'emoji'
    $aliasList = @(@(Get-WtwPropertyValue -Object $Snapshot -Name 'aliases') | ForEach-Object { "$_".Trim() } | Where-Object { $_ })
    $registeredBranch = if ($detached -or -not $branch) { '(detached)' } else { $branch }

    $metaSplat = @{
        RepoName     = $repoName
        RepoEntry    = $repoEntry
        Task         = $task
        Branch       = $registeredBranch
        WorktreePath = $worktreePath
        FolderSuffix = $suffix
    }
    if ($pretty) { $metaSplat['PrettyName'] = $pretty }
    if ($color) { $metaSplat['Color'] = $color }
    if ($emoji) { $metaSplat['Emoji'] = $emoji }
    if ($aliasList.Count -gt 0) { $metaSplat['Alias'] = $aliasList }

    $meta = Initialize-WtwWorktreeMetadata @metaSplat
    if (-not $meta.Success) {
        git -C $mainPath worktree remove $worktreePath --force 2>$null | Out-Null
        return
    }

    Write-WtwHost ''
    Write-WtwHost " Imported '$task' under $repoName from $SourceName." -ForegroundColor Green
    Write-WtwHost " Use 'wtw go $task' to switch." -ForegroundColor Green
    Write-WtwHost ''
}