Netscoot.Core/Public/Move-Solution.ps1
|
function Move-Solution { <# .SYNOPSIS Move a solution file (.sln/.slnx) and rebase the relative project paths it stores, so every project it references still resolves from the solution's new location. .DESCRIPTION A solution stores each project, of any project type, and each solution item as a path relative to the solution file. Moving the solution changes that base directory, so every stored path is recomputed. The dotnet CLI has no "rebase" command, so this rewrites the stored paths in place, keeping the file's formatting and encoding. It replaces the exact path token captured from the file (the .slnx `<Project Path="...">` or `<File Path="...">`, or the .sln project or SolutionItems line), and keeps each format's separator convention (/ for .slnx, \ for .sln). A web site project stored as a URL is left alone. Project-to-project references are unaffected by a solution move and are left alone. git is used when available (else confirmed plain-move fallback via -Force). -WhatIf supported. dotnet is not required. .PARAMETER Path The .sln/.slnx file to move. Accepts a path string or a Get-ChildItem/Get-Item item from the pipeline, and rejects other object types. .PARAMETER Destination New file path (or a folder, in which case the solution keeps its name). .PARAMETER Force When git is not installed, move with a plain PowerShell `Move-Item` without asking first. Without -Force it asks before falling back. The plain move does not preserve git history. .PARAMETER NoJournal Skip recording this move in the undo journal for this call, even when journaling is enabled (Undo-Netscoot will not see this move). .OUTPUTS Netscoot.SolutionMoveResult .EXAMPLE # Preview moving a solution and rebasing the project paths it stores Move-Solution -Path ./Demo.slnx -Destination ./build/Demo.slnx -WhatIf # Destination is an existing folder -> lands at ./build/Demo.slnx Move-Solution -Path ./Demo.slnx -Destination ./build # Works the same for .sln Move-Solution -Path ./Demo.sln -Destination ./build/Demo.sln #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] [OutputType('Netscoot.SolutionMoveResult')] param( [Parameter(Mandatory, Position = 0, ValueFromPipeline)] [Netscoot.PathInputTransform()] [ValidateNotNullOrEmpty()] [string]$Path, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Destination, [switch]$Force, [switch]$NoJournal ) process { $src = Resolve-FullPath $Path if (-not (Test-Path -LiteralPath $src -PathType Leaf)) { $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new( [System.IO.FileNotFoundException]::new("Solution not found: $Path"), 'SolutionNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $Path)) return } $ext = [System.IO.Path]::GetExtension($src) if ($ext -notin '.sln', '.slnx') { $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new( [System.ArgumentException]::new("Not a solution file (.sln/.slnx): $Path"), 'NotASolution', [System.Management.Automation.ErrorCategory]::InvalidArgument, $Path)) return } $name = Split-Path -Leaf $src # git mv semantics (shared by every mover): existing dir -> move into it; else rename. $newPath = Resolve-MoveTarget -Source $src -Destination $Destination if (Test-Path -LiteralPath $newPath) { $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new( [System.IO.IOException]::new("Destination already exists: $newPath"), 'DestinationExists', [System.Management.Automation.ErrorCategory]::ResourceExists, $newPath)) return } $entries = @(Get-SolutionProjectEntries -SolutionFile $src) $solutionItems = @(Get-SolutionItemEntries -SolutionFile $src) Write-MovePlan -Cmdlet $PSCmdlet -Caption "Move-Solution $name $src -> $newPath" -Items ([ordered]@{ 'project paths to rebase' = @($entries | ForEach-Object { $_.Raw }) 'solution item paths to rebase' = @($solutionItems | ForEach-Object { $_.Raw }) }) $performed = $false $rebased = 0 $itemsRebased = 0 $skippedCount = 0 if ($PSCmdlet.ShouldProcess("$src -> $newPath", "Move solution and rebase $($entries.Count + $solutionItems.Count) stored path(s)")) { $ctx = Resolve-MoveContext -Cmdlet $PSCmdlet -Force:$Force -TargetForError $src if (-not $ctx) { return } $repoFull = Get-RepositoryRoot -StartPath (Split-Path -Parent $src) # The solution-path rebases happen after the move, so they are Reattach-only items. $counter = @{ N = 0 } $itemCounter = @{ N = 0 } $rebaseSb = { param($Ref, $NewFile, $Counter) if ($Ref.FollowFile($NewFile)) { $Counter.N++ } } $items = @() foreach ($e in $entries) { $items += New-MoveItem -Description "rebase path: $($e.Raw) -> $($e.RawFollowing($newPath))" ` -Reattach $rebaseSb -ReattachArgs @($e, $newPath, $counter) } foreach ($e in $solutionItems) { $items += New-MoveItem -Description "rebase solution item: $($e.Raw) -> $($e.RawFollowing($newPath))" ` -Reattach $rebaseSb -ReattachArgs @($e, $newPath, $itemCounter) } $move = { param($UseGit, $Src, $Dst, $Repository) Move-PathTracked -UseGit $UseGit -Source $Src -Destination $Dst -RepositoryRoot $Repository } $planResult = Invoke-MovePlan -Caption "Move solution $name" -Items $items -Move $move ` -MoveArgs @($ctx.UseGit, $src, $newPath, $repoFull) ` -BackupPath @($src) -Rollback $move -RollbackArgs @($ctx.UseGit, $newPath, $src, $repoFull) ` -RepositoryRoot $repoFull -Command 'Move-Solution' -Engine 'dotnet' -Source $src -Destination $newPath ` -UndoParams @{ Path = $newPath; Destination = $src; Force = [bool]$Force } -NoJournal:$NoJournal $performed = $true $rebased = $counter.N $itemsRebased = $itemCounter.N $skippedCount = $planResult.Skipped } New-MoveResult -TypeName 'Netscoot.SolutionMoveResult' -Engine 'dotnet' -Source $src -Destination $newPath ` -Performed $performed -SkippedCount $skippedCount -Extra ([ordered]@{ ProjectsRebased = $rebased ItemsRebased = $itemsRebased }) } } |