Private/ExportWorker.ps1
|
<#
.SYNOPSIS Self-contained worker logic for processing a single TFVC changeset. .DESCRIPTION This function processes the details of a single changeset by querying its file changes and associated work items, evaluating them against the mapping configuration, and returning a consolidated changeset object. It is extracted into its own file so it can be easily dot-sourced into parallel runspaces and unit-tested independently. #> function Invoke-ExportWorker { [CmdletBinding()] param( [Parameter(Mandatory)][object]$Changeset, [Parameter(Mandatory)][hashtable]$Connection, [Parameter(Mandatory)][object]$Config ) # --- Helper: find the mapping for a server path --- function Find-SourceMapping { param([string]$ServerPath) $sp = $ServerPath.Replace('\', '/').TrimEnd('/') foreach ($m in $Config.sourceMappings) { $base = $m.tfvcPath.Replace('\', '/').TrimEnd('/') if ($sp.StartsWith($base, [StringComparison]::OrdinalIgnoreCase)) { return $m } } return $null } # --- Helper: normalise changeType to primary type --- function Get-PrimaryChangeType { param([string]$RawChangeType) $types = $RawChangeType -split ',' | ForEach-Object { $_.Trim() } if ($types -contains 'delete') { return 'delete' } if ($types -contains 'sourcerename') { return 'delete' } if ($types -contains 'rename') { return 'rename' } if ($types -contains 'add') { return 'add' } if ($types -contains 'branch') { return 'branch' } if ($types -contains 'undelete') { return 'undelete' } if ($types -contains 'merge') { return 'merge' } return 'edit' } try { $changes = @(Get-TfvcChangesetChanges -Connection $Connection -ChangesetId $Changeset.changesetId) $workItems = @(Get-TfvcChangesetWorkItems -Connection $Connection -ChangesetId $Changeset.changesetId) } catch { # Return the error details, making sure to include the fully qualified exception message return [PSCustomObject]@{ Error = "ERROR fetching details for changeset $($Changeset.changesetId): $_" } } # Filter to in-scope file changes $scopedChanges = [System.Collections.Generic.List[object]]::new() foreach ($change in $changes) { if ($null -eq $change.psobject.Properties['item']) { continue } if ($null -eq $change.item.psobject.Properties['path']) { continue } $serverPath = $change.item.path if (-not $serverPath) { continue } $isFolder = ($null -ne $change.item.psobject.Properties['isFolder'] -and $change.item.isFolder -eq $true) $isTree = ($null -ne $change.item.psobject.Properties['gitObjectType'] -and $change.item.gitObjectType -eq 'tree') $changeType = Get-PrimaryChangeType -RawChangeType $change.changeType if ($isFolder -or $isTree) { # Check if this folder change affects any of our mappings $affectedMappings = @() foreach ($m in $Config.sourceMappings) { if ($serverPath -eq $m.tfvcPath -or $serverPath.StartsWith("$($m.tfvcPath)/", 'CurrentCultureIgnoreCase')) { $affectedMappings += $m } elseif ($m.tfvcPath.StartsWith("$serverPath/", 'CurrentCultureIgnoreCase')) { $affectedMappings += $m } } if ($affectedMappings.Count -gt 0) { if ($changeType -in @('add', 'branch', 'rename', 'undelete')) { foreach ($m in $affectedMappings) { $fetchPath = if ($m.tfvcPath.StartsWith("$serverPath/", 'CurrentCultureIgnoreCase')) { $m.tfvcPath } else { $serverPath } $items = Get-TfvcItems -Connection $Connection -ScopePath $fetchPath -ChangesetVersion $Changeset.changesetId -RecursionLevel 'Full' foreach ($item in $items) { if ($null -ne $item.psobject.Properties['isFolder'] -and $item.isFolder -eq $true) { continue } if ($null -ne $item.psobject.Properties['gitObjectType'] -and $item.gitObjectType -eq 'tree') { continue } $destPath = ConvertTo-RelativePath -ServerPath $item.path -TfvcBase $m.tfvcPath -DestinationPrefix $(if ($m.destinationPath) { $m.destinationPath } else { '' }) if ($destPath) { $scopedChanges.Add([PSCustomObject]@{ changeType = 'add'; serverPath = $item.path; destinationPath = $destPath; sourceServerPath = $null; branch = (Get-MappingBranch -Mapping $m) }) } } } } elseif ($changeType -eq 'delete') { foreach ($m in $affectedMappings) { $fetchPath = if ($m.tfvcPath.StartsWith("$serverPath/", 'CurrentCultureIgnoreCase')) { $m.tfvcPath } else { $serverPath } $prev = $Changeset.changesetId - 1 if ($prev -gt 0) { $items = Get-TfvcItems -Connection $Connection -ScopePath $fetchPath -ChangesetVersion $prev -RecursionLevel 'Full' foreach ($item in $items) { if ($null -ne $item.psobject.Properties['isFolder'] -and $item.isFolder -eq $true) { continue } if ($null -ne $item.psobject.Properties['gitObjectType'] -and $item.gitObjectType -eq 'tree') { continue } $destPath = ConvertTo-RelativePath -ServerPath $item.path -TfvcBase $m.tfvcPath -DestinationPrefix $(if ($m.destinationPath) { $m.destinationPath } else { '' }) if ($destPath) { $scopedChanges.Add([PSCustomObject]@{ changeType = 'delete'; serverPath = $item.path; destinationPath = $destPath; sourceServerPath = $null; branch = (Get-MappingBranch -Mapping $m) }) } } } } } } continue } # Must be under one of our configured paths $mapping = Find-SourceMapping -ServerPath $serverPath if (-not $mapping) { continue } $destPath = ConvertTo-RelativePath -ServerPath $serverPath -TfvcBase $mapping.tfvcPath -DestinationPrefix $(if ($mapping.destinationPath) { $mapping.destinationPath } else { '' }) if (-not $destPath) { continue } $sourceServerPath = $null if ($changeType -eq 'rename' -and $null -ne $change.psobject.Properties['sourceServerItem']) { if ($null -ne $change.sourceServerItem.psobject.Properties['path']) { $sourceServerPath = $change.sourceServerItem.path } } $scopedChanges.Add([PSCustomObject]@{ changeType = $changeType serverPath = $serverPath destinationPath = $destPath sourceServerPath = $sourceServerPath branch = (Get-MappingBranch -Mapping $mapping) }) } # Deduplicate changes for the same file in the same changeset. Key on # branch + destinationPath so the same relative path on two branches # (e.g. /DEV and /Prod both at root) does not collide. $uniqueChanges = @{} foreach ($c in $scopedChanges) { $key = "$($c.branch)|$($c.destinationPath)" if ($c.changeType -eq 'delete') { $uniqueChanges[$key] = $c } elseif (-not $uniqueChanges.ContainsKey($key)) { $uniqueChanges[$key] = $c } else { if ($c.changeType -ne 'add') { $uniqueChanges[$key] = $c } } } $scopedChanges = $uniqueChanges.Values $wiList = @($workItems | ForEach-Object { [PSCustomObject]@{ id = $_.id; title = $_.title } }) $authorName = "Unknown" if ($null -ne $Changeset.psobject.Properties['author']) { $authorName = "$($Changeset.author)" if ($null -ne $Changeset.author.psobject.Properties['displayName']) { $authorName = $Changeset.author.displayName } elseif ($null -ne $Changeset.author.psobject.Properties['uniqueName']) { $authorName = $Changeset.author.uniqueName } } $comment = if ($null -ne $Changeset.psobject.Properties['comment']) { $Changeset.comment } else { '' } $createdDate = if ($null -ne $Changeset.psobject.Properties['createdDate']) { $Changeset.createdDate } else { '' } return [PSCustomObject]@{ changesetId = $Changeset.changesetId author = $authorName createdDate = $createdDate comment = $comment workItems = $wiList changes = @($scopedChanges) Error = $null } } |