<# .SYNOPSIS Sincronizeaza in ARBORE (langa binare) versiunile text FoxBin2Prg ale binarelor VFP ale unui proiect ROA, pentru ca git sa urmareasca text lizibil in loc de binare. .DESCRIPTION Folder-mode recursiv pe TOT arborele proiectului (inclusiv COMUN\), NU project-mode din .pjx: altfel binarele nereferentiate in .pjx ar disparea din git. Pentru fiecare binar suportat genereaza fisierul text (.vc2/.sc2/.fr2/.mn2/.lb2/.pj2/.dc2/.db2) IN ACELASI FOLDER cu binarul. Tipuri convertite: vcx, scx, frx, mnx, lbx, pjx, dbc (recursiv) + dbf-urile dintr-o lista explicita per proiect (-DbfList). Incremental: sare peste text-ul deja la zi (mtime binar vs text). CONVERSIE IN STAGING TEMP (nu in arbore): FoxBin2Prg.EXE lasa artefacte (.err, _*.BAK) si vcx2txt sterge copia binara la final - cu destinatia = arborele ar sterge binarul din working copy-ul SVN. Aici copiem binar+memo (+.dbf.cfg) intr-un temp, convertim acolo, si copiem inapoi DOAR textul (raw bytes, byte-preserving - encoding real cp1250 sub header CPID=1252, nu decodam). Binarele din arbore NU sunt atinse niciodata. Fidelitate per tip: - vcx/scx: roundtrip complet (bin->text->bin->text, comparare pe octeti) - au write-back. - frx/mnx/lbx/pjx/dbc/dbf: doar bin->text cu exit-code + timeout + text nevid (write-back nesuportat). Esec partial: la esecul unui fisier continua cu restul, colecteaza esecurile, le raporteaza la final si iese cu exit code nenul. Curatare orfani: textele .??2 fara binar corespondent (binar sters din SVN) si .db2-urile ale caror tabele au IESIT din lista -DbfList sunt DOAR RAPORTATE implicit; se sterg numai cu -OrphanCleanup explicit. Toate invocarile FoxBin2Prg.EXE trec prin Invoke-FoxBin2PrgSafe (timeout + taskkill). .PARAMETER ProjectRoot Radacina proiectului (implicit D:\ROA\ROACONT). .PARAMETER Types Tipurile binare (fara dbf) convertite recursiv (implicit vcx,scx,frx,mnx,lbx,pjx,dbc). .PARAMETER DbfList Lista de pattern-uri relative la ProjectRoot pentru dbf-urile convertite in .db2 cu DATE (implicit lista ROACONT: Locale\locale*.dbf, Help\*.dbf, DATE\Optiuni_*.dbf). .PARAMETER SubDir Optional. Limiteaza rularea la un subfolder (relativ la ProjectRoot) - util pentru rulari in pasi. Curatarea de orfani se face doar cand se ruleaza pe tot arborele (SubDir gol). .PARAMETER TimeoutSeconds Timeout per invocare FoxBin2Prg (implicit 60s). .PARAMETER MaxDb2Mb Prag: un .db2 peste aceasta dimensiune e raportat (tabela ar trebui scoasa din lista). Implicit 5. .PARAMETER Force Reconverteste chiar daca textul e la zi. .PARAMETER NoOrphanCleanup Pastrat pentru compatibilitate: forteaza doar-raportare chiar si cu -OrphanCleanup. .PARAMETER OrphanCleanup Sterge efectiv textele orfane. Implicit orfanii sunt DOAR raportati, nu stersi. .EXAMPLE powershell -ExecutionPolicy Bypass -File D:\ROA\UTIL\foxbin2prg\git_sync.ps1 -ProjectRoot D:\ROA\ROACONT #> [CmdletBinding()] param( [string]$ProjectRoot = 'D:\ROA\ROACONT', [string[]]$Types = @('vcx','scx','frx','mnx','lbx','pjx','dbc'), [string[]]$DbfList = @('Locale\locale*.dbf','Help\*.dbf','DATE\Optiuni_*.dbf', 'COMUN\clase\Locale\*.dbf','COMUN\datemenu\*.dbf','COMUN\datemenu\xold\*.dbf', 'COMUN\Drepturi utilizatori\*\*.dbf','COMUN\utile\GridExtras\gridextras.dbf', 'COMUN\utile\ListProperty\lproperty.dbf','COMUN\utile\hpdf\*.dbf','COMUN\utile\hpdf\ReportOutput\*.dbf', # acelasi set si fara prefixul COMUN\, pentru rulari cu -ProjectRoot chiar pe radacina COMUN 'clase\Locale\*.dbf','datemenu\*.dbf','datemenu\xold\*.dbf', 'Drepturi utilizatori\*\*.dbf','utile\GridExtras\gridextras.dbf', 'utile\ListProperty\lproperty.dbf','utile\hpdf\*.dbf','utile\hpdf\ReportOutput\*.dbf'), # clase FFC/third-party cu #INCLUDE: roundtrip izolat = fals-negativ; editare doar in IDE, fara write-back [string[]]$RoundtripExempt = @('COMUN\clase\accessibility.vcx','COMUN\clase\oinventar.vcx','COMUN\clase\_gdiplus.vcx','COMUN\clase\_reportlistener.vcx','COMUN\utile\foxcharts\foxcharts.vcx','COMUN\utile\web\_webview.vcx'), [string]$SubDir = '', [int]$TimeoutSeconds = 60, [int]$MaxDb2Mb = 5, [switch]$Force, [switch]$NoOrphanCleanup, [switch]$OrphanCleanup ) $ErrorActionPreference = 'Stop' $exe = 'D:\ROA\UTIL\foxbin2prg\FoxBin2Prg.EXE' if (-not (Test-Path $exe)) { throw "Nu gasesc $exe. Compileaza intai FoxBin2Prg (deschide foxbin2prg.pj2 in VFP si Build)." } if (-not (Test-Path -LiteralPath $ProjectRoot -PathType Container)) { throw "ProjectRoot nu exista: $ProjectRoot" } . (Join-Path $PSScriptRoot 'vfp_filemap.ps1') $map = $script:VfpFileMap $ProjectRoot = (Get-Item -LiteralPath $ProjectRoot).FullName # extensiile text valide (pt curatare orfani), lowercase $script:TextExts = @{} foreach ($k in $map.Keys) { $script:TextExts[$map[$k][1]] = $k } # .vc2 -> .vcx # set cai absolute (lowercase) ale binarelor scutite de garda roundtrip - doar bin->text valid $script:ExemptSet = New-Object System.Collections.Generic.HashSet[string] foreach ($rel in $RoundtripExempt) { [void]$script:ExemptSet.Add((Join-Path $ProjectRoot $rel).ToLower()) } $runStamp = "{0}-{1}" -f $PID, (Get-Date -Format 'yyyyMMddHHmmssfff') $stagingRoot = Join-Path $env:TEMP ("git_sync_" + (Split-Path $ProjectRoot -Leaf) + "\$runStamp") function Invoke-Fb2p { # toate apelurile trec prin watchdog (timeout + taskkill) - vezi vfp_filemap.ps1 param([string]$InputFile, [string]$Recompile = '') if ($Recompile) { $r = Invoke-FoxBin2PrgSafe -ExePath $exe -ExeArgs @($InputFile,'','','','1','0','1','',$Recompile) -TimeoutSeconds $TimeoutSeconds } else { $r = Invoke-FoxBin2PrgSafe -ExePath $exe -ExeArgs @($InputFile,'','','','1','0','1') -TimeoutSeconds $TimeoutSeconds } return $r } function Find-InDir([string]$dir, [string]$name) { # gaseste un fisier dupa nume (case-insensitive) intr-un folder; $null daca lipseste if (-not (Test-Path -LiteralPath $dir -PathType Container)) { return $null } return (Get-ChildItem -LiteralPath $dir -Filter $name -File -ErrorAction SilentlyContinue | Select-Object -First 1) } function Test-BytesEqual([byte[]]$a, [byte[]]$b) { # normalizeaza CRLF/CR->LF si compara ca string Latin1 (byte<->char lossless) - rapid pe fisiere mari; # bucla per-octet in PowerShell era patologic de lenta (minute/fisier) pe textele de sute de KB. $enc = [System.Text.Encoding]::GetEncoding(28591) $sa = $enc.GetString($a) -replace "`r`n?", "`n" $sb = $enc.GetString($b) -replace "`r`n?", "`n" return $sa -eq $sb } # --- Converteste un binar in staging si copiaza inapoi DOAR textul. Intoarce status string. --- function Convert-One { param([Parameter(Mandatory)][System.IO.FileInfo]$Bin) $ext = $Bin.Extension.ToLower() $memoX = $map[$ext][0] $txtX = $map[$ext][1] $dir = $Bin.DirectoryName $base = [IO.Path]::GetFileNameWithoutExtension($Bin.Name) $memo = Find-InDir $dir ($base + $memoX) # text tinta existent (case-insensitive) pentru incremental + destinatie $txtExisting = Find-InDir $dir ($base + $txtX) if (-not $Force -and $txtExisting) { $newestBin = $Bin.LastWriteTime if ($memo -and $memo.LastWriteTime -gt $newestBin) { $newestBin = $memo.LastWriteTime } if ($txtExisting.LastWriteTime -ge $newestBin) { return 'uptodate' } } $stageDir = Join-Path $stagingRoot ([Guid]::NewGuid().ToString('N').Substring(0,12)) New-Item -ItemType Directory -Force -Path $stageDir | Out-Null try { # cfg in staging (mostenit si de subfolderul rc): DontShowErrors suprima MessageBox-ul propriu # "End of Process (with errors)"; ShowProgressbar 0 = fara fereastra de progres (rulare headless). $cfgLines = [System.Collections.Generic.List[string]]@('DontShowErrors: 1','ShowProgressbar: 0') if ($ext -eq '.dbf') { $cfgLines.Add('DBF_Conversion_Support: 4') } # forteaza exportul DATELOR Set-Content -LiteralPath (Join-Path $stageDir 'foxbin2prg.cfg') -Value $cfgLines -Encoding Ascii $stageBin = Join-Path $stageDir $Bin.Name Copy-Item -LiteralPath $Bin.FullName -Destination $stageBin -Force if ($memo) { Copy-Item -LiteralPath $memo.FullName -Destination (Join-Path $stageDir $memo.Name) -Force } if ($ext -eq '.dbc') { # dbc = tabela cu index structural .dcx; fara el FoxBin2Prg cere indexul intr-un dialog modal (hang) $dcx = Find-InDir $dir ($base + '.dcx') if ($dcx) { Copy-Item -LiteralPath $dcx.FullName -Destination (Join-Path $stageDir $dcx.Name) -Force } } if ($ext -eq '.dbf') { # index structural + (daca e tabela legata de DBC) containerul: FoxBin2Prg le cere ca sa deschida tabela $cdx = Find-InDir $dir ($base + '.cdx') if ($cdx) { Copy-Item -LiteralPath $cdx.FullName -Destination (Join-Path $stageDir $cdx.Name) -Force } Get-ChildItem -LiteralPath $dir -File -ErrorAction SilentlyContinue | Where-Object { '.dbc','.dct','.dcx' -contains $_.Extension.ToLower() } | ForEach-Object { Copy-Item -LiteralPath $_.FullName -Destination (Join-Path $stageDir $_.Name) -Force } # onoreaza order/condition custom din .dbf.cfg comis, daca exista $cfg = Find-InDir $dir ($base + '.dbf.cfg') if ($cfg) { Copy-Item -LiteralPath $cfg.FullName -Destination (Join-Path $stageDir $cfg.Name) -Force } } $r = Invoke-Fb2p -InputFile $stageBin if ($r.TimedOut) { return 'fail:timeout bin->text' } if ($r.ExitCode -ne 0) { return "fail:bin->text ErrorLevel=$($r.ExitCode)" } $stageTxt = Find-InDir $stageDir ($base + $txtX) if (-not $stageTxt) { return 'fail:text negenerat' } if ($stageTxt.Length -eq 0) { return 'fail:text gol' } # scutite (clase FFC cu #INCLUDE): roundtrip izolat = fals-negativ; doar bin->text valid, fara write-back $exempt = $script:ExemptSet.Contains($Bin.FullName.ToLower()) # fidelitate per tip: roundtrip complet doar vcx/scx (mai putin cele scutite) if (($ext -eq '.vcx' -or $ext -eq '.scx') -and -not $exempt) { $rc2 = Join-Path $stageDir 'rc' New-Item -ItemType Directory -Force -Path $rc2 | Out-Null $txt1 = Join-Path $rc2 $stageTxt.Name Copy-Item -LiteralPath $stageTxt.FullName -Destination $txt1 -Force $r2 = Invoke-Fb2p -InputFile $txt1 -Recompile $ProjectRoot if ($r2.TimedOut -or $r2.ExitCode -ne 0) { return "fail:roundtrip text->bin ErrorLevel=$($r2.ExitCode)" } $bin2 = Find-InDir $rc2 ($base + $ext) if (-not $bin2) { return 'fail:roundtrip bin nerecreat' } $r3 = Invoke-Fb2p -InputFile $bin2.FullName if ($r3.TimedOut -or $r3.ExitCode -ne 0) { return "fail:roundtrip bin->text2 ErrorLevel=$($r3.ExitCode)" } $txt2 = Find-InDir $rc2 ($base + $txtX) if (-not $txt2) { return 'fail:roundtrip text2 negenerat' } if (-not (Test-BytesEqual ([IO.File]::ReadAllBytes($stageTxt.FullName)) ([IO.File]::ReadAllBytes($txt2.FullName)))) { return 'fail:roundtrip text1 != text2' } } if ($ext -eq '.dbf' -and $stageTxt.Length -gt ($MaxDb2Mb * 1MB)) { $script:BigDb2 += ("{0} ({1:N1} MB)" -f (Get-RelPath $Bin.FullName), ($stageTxt.Length / 1MB)) } # copiaza inapoi DOAR textul (raw bytes), pastrand numele produs de FoxBin2Prg $destTxt = if ($txtExisting) { $txtExisting.FullName } else { Join-Path $dir $stageTxt.Name } Copy-Item -LiteralPath $stageTxt.FullName -Destination $destTxt -Force # mtime text >= mtime binar, altfel pare stale si se reconverteste mereu $destItem = Get-Item -LiteralPath $destTxt $newestBin = $Bin.LastWriteTime if ($memo -and $memo.LastWriteTime -gt $newestBin) { $newestBin = $memo.LastWriteTime } if ($destItem.LastWriteTime -lt $newestBin) { $destItem.LastWriteTime = $newestBin.AddSeconds(1) } if ($exempt) { return 'ok-exempt' } return 'ok' } finally { Remove-Item -Recurse -Force -LiteralPath $stageDir -ErrorAction SilentlyContinue } } function Get-RelPath([string]$full) { if ($full.ToLower().StartsWith($ProjectRoot.ToLower())) { return $full.Substring($ProjectRoot.Length).TrimStart('\') } return $full } # =========================== MAIN =========================== $scanRoot = if ($SubDir) { Join-Path $ProjectRoot $SubDir } else { $ProjectRoot } if (-not (Test-Path -LiteralPath $scanRoot)) { throw "SubDir nu exista: $scanRoot" } Write-Host "=== git_sync: ProjectRoot=$ProjectRoot SubDir=$(if($SubDir){$SubDir}else{'(tot)'}) Types=$($Types -join ',') ===" # --- lista dbf din patternurile per proiect (cai absolute, set pt orfani) --- $script:DbfInList = New-Object System.Collections.Generic.HashSet[string] foreach ($pat in $DbfList) { $full = Join-Path $ProjectRoot $pat Get-ChildItem -Path $full -File -ErrorAction SilentlyContinue | Where-Object { $_.Extension.ToLower() -eq '.dbf' } | ForEach-Object { [void]$script:DbfInList.Add($_.FullName.ToLower()) } } # --- construieste lista de binare de convertit --- $script:BigDb2 = @() $binList = @() $typeExts = $Types | ForEach-Object { '.' + $_.ToLower() } $binList += Get-ChildItem -LiteralPath $scanRoot -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $typeExts -contains $_.Extension.ToLower() } # dbf-urile din lista aflate sub scanRoot $binList += $script:DbfInList | ForEach-Object { Get-Item -LiteralPath $_ -ErrorAction SilentlyContinue } | Where-Object { $_ -and $_.FullName.ToLower().StartsWith(((Get-Item -LiteralPath $scanRoot).FullName).ToLower()) } $ok=0; $up=0; $failures=@() foreach ($b in $binList) { $status = Convert-One -Bin $b $rel = Get-RelPath $b.FullName switch -Wildcard ($status) { 'ok' { $ok++; Write-Host "OK $rel" } 'ok-exempt' { $ok++; Write-Host "OK $rel (roundtrip scutit)" } 'uptodate' { $up++ } 'fail:*' { $failures += [PSCustomObject]@{ File=$rel; Reason=$status.Substring(5) }; Write-Warning "ESEC $rel -- $($status.Substring(5))" } } } # --- curatare orfani (doar pe tot arborele) --- $orphansDeleted = @() if (-not $SubDir) { $allText = Get-ChildItem -LiteralPath $ProjectRoot -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $script:TextExts.ContainsKey($_.Extension.ToLower()) } foreach ($t in $allText) { $tExt = $t.Extension.ToLower() $binExt = $script:TextExts[$tExt] $base = [IO.Path]::GetFileNameWithoutExtension($t.Name) $bin = Find-InDir $t.DirectoryName ($base + $binExt) $isOrphan = $false if (-not $bin) { $isOrphan = $true # binar sters } elseif ($tExt -eq '.db2' -and -not $script:DbfInList.Contains($bin.FullName.ToLower())) { $isOrphan = $true # tabela a iesit din lista } if ($isOrphan) { $orphansDeleted += (Get-RelPath $t.FullName) if ($OrphanCleanup -and -not $NoOrphanCleanup) { Remove-Item -LiteralPath $t.FullName -Force -ErrorAction SilentlyContinue } } } } Remove-Item -Recurse -Force -LiteralPath $stagingRoot -ErrorAction SilentlyContinue # --- raport --- Write-Host '' Write-Host "--- Sumar git_sync ---" Write-Host "Convertite: $ok Sarite(la zi): $up Esuate: $($failures.Count)" if ($orphansDeleted.Count -gt 0) { $verb = if ($OrphanCleanup -and -not $NoOrphanCleanup) { 'sterse' } else { 'raportate (nesterse)' } Write-Host "Orfani ${verb}: $($orphansDeleted.Count)" $orphansDeleted | ForEach-Object { Write-Host " orfan: $_" } } if ($script:BigDb2.Count -gt 0) { Write-Warning "db2 peste ${MaxDb2Mb} MB (scoate tabela din lista, ramane binara):" $script:BigDb2 | ForEach-Object { Write-Warning " $_" } } if ($failures.Count -gt 0) { Write-Host '' Write-Host "ESECURI (nu comite pana nu sunt explicate):" $failures | ForEach-Object { Write-Host " $($_.File) -- $($_.Reason)" } exit 1 } Write-Host "OK: fara esecuri." exit 0