Files
foxbin2prg/git_sync.ps1
Marius Mutu a05687a38f Flux git-text in-arbore: git_sync.ps1 + teste, txt2vcx in-place, filemap pjx/dbf
- git_sync.ps1 nou: conversie recursiva bin->text in arbore (staging temp,
  incremental, orfani, RoundtripExempt, DbfList cu date, anti-dialog cfg)
- vfp_filemap.ps1: mapari .pjx->.pj2 si .dbf->.db2
- txt2vcx.ps1: CacheRoot implicit = ProjectRoot (text in-arbore) + cfg anti-dialog in staging
- test_roundtrip.ps1: parametrizat per proiect, restaurare din backup temp (nu git checkout)
- test_git_sync.ps1 nou: incremental, orfani, esec partial, protectie binar, scutire roundtrip
- CLAUDE.md: fluxul nou documentat

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 11:04:23 +03:00

314 lines
15 KiB
PowerShell

<#
.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) se sterg si se
raporteaza; la fel .db2-urile ale caror tabele au IESIT din lista -DbfList.
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
Nu sterge textele orfane (doar le raporteaza).
.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'),
# 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
)
$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 (-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 ($NoOrphanCleanup) { 'raportate (nesterse)' } else { 'sterse' }
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