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>
This commit is contained in:
313
git_sync.ps1
Normal file
313
git_sync.ps1
Normal file
@@ -0,0 +1,313 @@
|
||||
<#
|
||||
.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
|
||||
Reference in New Issue
Block a user