From 51cab198c4abff0b817f8d229c8bd3cf26eebf70 Mon Sep 17 00:00:00 2001 From: Marius Mutu Date: Mon, 27 Jul 2026 11:52:23 +0300 Subject: [PATCH] roa_sync: reconciliere cu origin + push gardat doar commit-uri de sync Dupa commit-ul local de sync: fetch origin, apoi ff-only (doar in urma) sau rebase --empty=drop (divergenta - sync-urile cu continut deja identic pe origin devin goale si sunt aruncate automat, fara interventie manuala); conflict real -> rebase --abort + exit nenul. Push origin main DOAR cand toate commit-urile ahead sunt de forma sync SVN[ r], cu maxim 3 reincercari la push respins (push simultan de pe alta masina). Switch nou -GitOnly (sare svn update + regenerarea textelor). Harness nou test_roa_sync.ps1: 8 scenarii pe fixture-uri git in TEMP, inclusiv reproducerea incidentului din 2026-07-27 (local byte-identic cu origin). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AvccPZXr4A9ihcTtExNoJT --- roa_sync.ps1 | 243 ++++++++++++++++++++++++++++++++++++---------- test_roa_sync.ps1 | 239 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 430 insertions(+), 52 deletions(-) create mode 100644 test_roa_sync.ps1 diff --git a/roa_sync.ps1 b/roa_sync.ps1 index b9b2858..cbd2318 100644 --- a/roa_sync.ps1 +++ b/roa_sync.ps1 @@ -7,19 +7,44 @@ 1. svn update pe ProjectRoot (include externalul COMUN\). Esec / conflicte -> stop, exit nenul. 2. git_sync.ps1 -ProjectRoot (improspateaza textele .??2, inclusiv COMUN). Esec (exit nenul) -> avertizare, fara commit-uri de sync, exit nenul. - 3. Repo proiect (ProjectRoot\.git): pe main comite "sync SVN r" daca sunt schimbari. - Pe alt branch: avertizare si skip commit (nu schimba branch-ul daca are modificari necomise; - daca working tree-ul e curat face checkout main). + 3. Repo proiect (ProjectRoot\.git): pe main comite "sync SVN r" daca sunt schimbari, + apoi reconciliaza cu origin si face push conditionat (vezi mai jos). 4. La fel repo COMUN (ProjectRoot\COMUN\.git), cu revizia din svn info pe folderul COMUN. 5. Inchide branch-urile claude/* terminate (git diff main..branch gol) in ambele repo-uri. - 6. Sumar final. + 6. Sumar final (commit / reconciliere / push per repo + avertismente). + + Reconcilierea cu origin (pasii 3-4, DUPA commit-ul local de sync - commit-ul intai face + tree-ul curat si aduce sub urmarire fisierele noi, deci nu mai apar blocajele clasice de + pull peste modificari necomise / fisiere netracked): + - git fetch origin; fara remote origin sau fetch esuat (offline) -> avertisment si + comportamentul vechi (doar commit local), exit 0. + - doar in urma fata de origin/main -> git merge --ff-only origin/main. + - divergenta -> git rebase origin/main. Commit-urile de sync al caror continut e deja + identic pe origin (alta masina a rulat acelasi svn update + sync + push inaintea + noastra) devin goale la rebase si sunt aruncate automat - NU e conflict si nu cere + interventie manuala. + - conflict REAL la rebase -> git rebase --abort, avertisment, exit nenul la final. + - push origin main DOAR daca toate commit-urile ahead de origin/main sunt commit-uri de + sync ("sync SVN[ r]" - continut oglindit din SVN, sursa de adevar). Orice commit + non-sync nepushed blocheaza push-ul automat (avertisment; push manual dupa review). + - push respins (push simultan de pe alta masina) -> reia fetch + reconciliere + push, + maxim 3 incercari, apoi avertisment (se reconciliaza la urmatoarea rulare). + + Exit: nenul la esec svn/git_sync sau conflict real cu origin; avertismentele (offline, + push sarit din cauza commit-urilor non-sync, push esuat) lasa exit 0 si apar in sumar. .PARAMETER ProjectRoot Radacina proiectului. Implicit: directorul curent, daca are .git si un .pjx. + +.PARAMETER GitOnly + Sare peste pasii SVN (1) si regenerarea textelor (2): doar commit + reconciliere + push + + inchidere branch-uri. Pentru reconciliere rapida cu origin sau pentru teste + (test_roa_sync.ps1) - nu cere working copy SVN si nici FoxBin2Prg. #> [CmdletBinding()] param( - [string]$ProjectRoot = '' + [string]$ProjectRoot = '', + [switch]$GitOnly ) $ErrorActionPreference = 'Stop' @@ -44,86 +69,193 @@ $ProjectRoot = (Get-Item -LiteralPath $ProjectRoot).FullName $comunRoot = Join-Path $ProjectRoot 'COMUN' $gitSync = Join-Path $PSScriptRoot 'git_sync.ps1' -Write-Host "=== roa_sync: ProjectRoot=$ProjectRoot ===" +Write-Host "=== roa_sync: ProjectRoot=$ProjectRoot$(if($GitOnly){' (-GitOnly)'}) ===" # --- helper: revizia SVN a unui folder --- function Get-SvnRev([string]$path) { + # EAP local Continue: sub Stop, redirectarea 2>$null a stderr-ului nativ arunca in PS 5.1 + $ErrorActionPreference = 'Continue' $rev = (& svn info --show-item revision $path 2>$null | Out-String).Trim() if ($LASTEXITCODE -ne 0 -or -not $rev) { return '' } return $rev } # =========================== 1. svn update =========================== +$rev = '' Write-Host '' -Write-Host "--- 1. svn update ---" -$svnOut = & svn update $ProjectRoot -$svnExit = $LASTEXITCODE -$svnOut | ForEach-Object { Write-Host " $_" } -if ($svnExit -ne 0) { - Write-Host "EROARE: svn update a esuat (exit $svnExit). Ma opresc." - exit 1 +if ($GitOnly) { + Write-Host "--- 1. svn update: sarit (-GitOnly) ---" +} else { + Write-Host "--- 1. svn update ---" + $svnOut = & svn update $ProjectRoot + $svnExit = $LASTEXITCODE + $svnOut | ForEach-Object { Write-Host " $_" } + if ($svnExit -ne 0) { + Write-Host "EROARE: svn update a esuat (exit $svnExit). Ma opresc." + exit 1 + } + # svn poate iesi 0 dar raporta conflicte in sumar + if ($svnOut | Where-Object { $_ -match 'Summary of conflicts|conflict' }) { + Write-Host "EROARE: svn update raporteaza conflicte. Rezolva-le manual, apoi reia. Ma opresc." + exit 1 + } + $rev = Get-SvnRev $ProjectRoot + Write-Host " Revizie SVN ProjectRoot: $(if($rev){$rev}else{'necunoscuta'})" } -# svn poate iesi 0 dar raporta conflicte in sumar -if ($svnOut | Where-Object { $_ -match 'Summary of conflicts|conflict' }) { - Write-Host "EROARE: svn update raporteaza conflicte. Rezolva-le manual, apoi reia. Ma opresc." - exit 1 -} -$rev = Get-SvnRev $ProjectRoot -Write-Host " Revizie SVN ProjectRoot: $(if($rev){$rev}else{'necunoscuta'})" # =========================== 2. git_sync =========================== Write-Host '' -Write-Host "--- 2. git_sync (texte .??2) ---" -if (-not (Test-Path -LiteralPath $gitSync)) { - Write-Host "EROARE: nu gasesc git_sync.ps1 la $gitSync. Ma opresc." - exit 1 -} -& powershell -ExecutionPolicy Bypass -NoProfile -File $gitSync -ProjectRoot $ProjectRoot -$syncExit = $LASTEXITCODE -if ($syncExit -ne 0) { - Write-Host '' - Write-Host "AVERTISMENT: git_sync a raportat esecuri (exit $syncExit). NU fac commit-uri de sync. Ma opresc." - exit 1 +if ($GitOnly) { + Write-Host "--- 2. git_sync: sarit (-GitOnly) ---" +} else { + Write-Host "--- 2. git_sync (texte .??2) ---" + if (-not (Test-Path -LiteralPath $gitSync)) { + Write-Host "EROARE: nu gasesc git_sync.ps1 la $gitSync. Ma opresc." + exit 1 + } + & powershell -ExecutionPolicy Bypass -NoProfile -File $gitSync -ProjectRoot $ProjectRoot + $syncExit = $LASTEXITCODE + if ($syncExit -ne 0) { + Write-Host '' + Write-Host "AVERTISMENT: git_sync a raportat esecuri (exit $syncExit). NU fac commit-uri de sync. Ma opresc." + exit 1 + } } -# =========================== 3-4. commit sync per repo =========================== -# Sincronizeaza un repo git: pe main comite schimbarile ca "sync SVN r". -# Intoarce: 'commit' / 'nimic' / 'skip-branch'. +# =========================== 3-4. commit sync + reconciliere origin per repo =========================== +# Pe main comite schimbarile ca "sync SVN r", apoi fetch + ff/rebase fata de origin/main +# (sync-urile cu continut deja identic pe origin devin goale la rebase si dispar) si push +# DOAR daca tot ahead-ul e din commit-uri de sync. Intoarce starea pasilor. function Sync-Repo { param([string]$RepoDir, [string]$Rev, [string]$Label) + # EAP local Continue: git scrie normal pe stderr (fetch/push/rebase), iar sub Stop + # redirectarea 2>$null ar arunca NativeCommandError in PS 5.1; verificam $LASTEXITCODE. + $ErrorActionPreference = 'Continue' + $r = [PSCustomObject]@{ Commit='nimic'; Origin='-'; Push='-'; Warnings=@() } if (-not (Test-Path -LiteralPath (Join-Path $RepoDir '.git'))) { Write-Host " ${Label}: fara repo git ($RepoDir) - sar peste." - return 'nimic' + $r.Commit = 'fara-repo' + return $r } $branch = (& git -C $RepoDir rev-parse --abbrev-ref HEAD).Trim() if ($branch -ne 'main') { # tree curat = putem trece pe main; altfel nu atingem branch-ul $dirty = @(& git -C $RepoDir status --porcelain).Count -gt 0 if ($dirty) { - Write-Host " ${Label}: esti pe '$branch'; sync-ul se face pe main. Ai modificari necomise - sar peste commit." - return 'skip-branch' + Write-Host " ${Label}: esti pe '$branch'; sync-ul se face pe main. Ai modificari necomise - sar peste." + $r.Commit = 'skip-branch' + return $r } Write-Host " ${Label}: pe '$branch' cu tree curat - fac checkout main." & git -C $RepoDir checkout main | Out-Null if ($LASTEXITCODE -ne 0) { - Write-Host " ${Label}: checkout main a esuat - sar peste commit." - return 'skip-branch' + Write-Host " ${Label}: checkout main a esuat - sar peste." + $r.Commit = 'skip-branch' + return $r } } & git -C $RepoDir add -A | Out-Null $staged = @(& git -C $RepoDir status --porcelain).Count -gt 0 - if (-not $staged) { - Write-Host " ${Label}: nimic de sincronizat." - return 'nimic' + if ($staged) { + $msg = if ($Rev) { "sync SVN r$Rev" } else { "sync SVN" } + & git -C $RepoDir commit -m $msg | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Host " ${Label}: commit a esuat - sar peste reconciliere." + $r.Commit = 'esec-commit' + $r.Warnings += "${Label}: commit de sync esuat" + return $r + } + Write-Host " ${Label}: commit '$msg'." + $r.Commit = 'commit' + } else { + Write-Host " ${Label}: nimic de comis." } - $msg = if ($Rev) { "sync SVN r$Rev" } else { "sync SVN" } - & git -C $RepoDir commit -m $msg | Out-Null + + # --- reconciliere cu origin (dupa commit: tree curat, fisierele noi sunt deja urmarite) --- + $originUrl = (& git -C $RepoDir remote get-url origin 2>$null | Out-String).Trim() + if ($LASTEXITCODE -ne 0 -or -not $originUrl) { + Write-Host " ${Label}: fara remote origin - repo doar local, sar peste reconciliere." + $r.Origin = 'fara-origin' + return $r + } + & git -C $RepoDir fetch origin 2>$null | Out-Null if ($LASTEXITCODE -ne 0) { - Write-Host " ${Label}: commit a esuat." - return 'skip-branch' + Write-Host " ${Label}: AVERTISMENT: git fetch origin a esuat (offline / gitea inaccesibil?); reconcilierea ramane pe urmatoarea rulare." + $r.Origin = 'fetch-esuat' + $r.Warnings += "${Label}: fetch origin esuat - reconciliere amanata" + return $r } - Write-Host " ${Label}: commit '$msg'." - return 'commit' + & git -C $RepoDir rev-parse --verify --quiet origin/main 2>$null | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Host " ${Label}: origin nu are inca main - sar peste reconciliere." + $r.Origin = 'fara-origin-main' + return $r + } + + for ($attempt = 1; $attempt -le 3; $attempt++) { + $behind = [int]((& git -C $RepoDir rev-list --count main..origin/main | Out-String).Trim()) + $ahead = [int]((& git -C $RepoDir rev-list --count origin/main..main | Out-String).Trim()) + if ($behind -gt 0 -and $ahead -eq 0) { + & git -C $RepoDir merge --ff-only origin/main 2>$null | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Host " ${Label}: AVERTISMENT: fast-forward la origin/main a esuat neasteptat - verifica manual." + $r.Origin = 'conflict' + $r.Warnings += "${Label}: fast-forward esuat - verifica manual repo-ul" + return $r + } + Write-Host " ${Label}: adus la zi cu origin/main (fast-forward, $behind commit-uri)." + $r.Origin = 'ff' + } elseif ($behind -gt 0 -and $ahead -gt 0) { + Write-Host " ${Label}: local si origin au divergat (ahead $ahead / behind $behind) - rebase pe origin/main." + & git -C $RepoDir rebase --empty=drop origin/main 2>$null | Out-Null + if ($LASTEXITCODE -ne 0) { + & git -C $RepoDir rebase --abort 2>$null | Out-Null + Write-Host " ${Label}: CONFLICT real la rebase pe origin/main - rebase anulat. Rezolva manual: git -C $RepoDir rebase origin/main" + $r.Origin = 'conflict' + $r.Warnings += "${Label}: conflict real cu origin/main - reconciliere manuala necesara" + return $r + } + $r.Origin = 'rebase' + } elseif ($r.Origin -eq '-') { + $r.Origin = if ($ahead -gt 0) { 'doar-ahead' } else { 'la-zi' } + } + + # --- push doar daca tot ahead-ul e din commit-uri de sync --- + $aheadMsgs = @(& git -C $RepoDir log --format=%s origin/main..main) | Where-Object { $_ -and $_.Trim() } + if ($aheadMsgs.Count -eq 0) { + Write-Host " ${Label}: la zi cu origin/main; nimic de impins." + $r.Push = 'nimic' + return $r + } + $nonSync = @($aheadMsgs | Where-Object { $_ -notmatch '^sync SVN( r\S+)?$' }) + if ($nonSync.Count -gt 0) { + Write-Host " ${Label}: AVERTISMENT: ahead de origin include commit-uri non-sync - NU fac push automat (push manual dupa review):" + $nonSync | Select-Object -First 5 | ForEach-Object { Write-Host " - $_" } + $r.Push = 'sarit-non-sync' + $r.Warnings += "${Label}: push sarit - exista commit-uri non-sync nepushed" + return $r + } + & git -C $RepoDir push origin main 2>$null | Out-Null + if ($LASTEXITCODE -eq 0) { + Write-Host " ${Label}: push origin main ($($aheadMsgs.Count) commit-uri de sync)." + $r.Push = 'push' + return $r + } + if ($attempt -lt 3) { + Write-Host " ${Label}: push respins (probabil push simultan de pe alta masina) - refetch si reiau (incercarea $($attempt+1))." + & git -C $RepoDir fetch origin 2>$null | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Host " ${Label}: AVERTISMENT: refetch esuat dupa push respins." + $r.Push = 'esec' + $r.Warnings += "${Label}: push esuat si refetch esuat - reconciliere la urmatoarea rulare" + return $r + } + } + } + Write-Host " ${Label}: AVERTISMENT: push respins de 3 ori - renunt; se reconciliaza la urmatoarea rulare." + $r.Push = 'esec' + $r.Warnings += "${Label}: push esuat dupa 3 incercari" + return $r } Write-Host '' @@ -133,9 +265,9 @@ $resProj = Sync-Repo -RepoDir $ProjectRoot -Rev $rev -Label 'proiect' Write-Host '' Write-Host "--- 4. sync repo COMUN ---" $revComun = '' -$resComun = 'nimic' +$resComun = [PSCustomObject]@{ Commit='nimic'; Origin='-'; Push='-'; Warnings=@() } if (Test-Path -LiteralPath $comunRoot -PathType Container) { - $revComun = Get-SvnRev $comunRoot + if (-not $GitOnly) { $revComun = Get-SvnRev $comunRoot } $resComun = Sync-Repo -RepoDir $comunRoot -Rev $revComun -Label 'COMUN' } else { Write-Host " COMUN: folderul nu exista ($comunRoot) - sar peste." @@ -173,10 +305,11 @@ if (Test-Path -LiteralPath $comunRoot -PathType Container) { # =========================== 6. sumar =========================== $commits = @() -if ($resProj -eq 'commit') { $commits += "proiect: sync SVN r$rev" } -if ($resComun -eq 'commit') { $commits += "COMUN: sync SVN r$revComun" } +if ($resProj.Commit -eq 'commit') { $commits += "proiect: sync SVN$(if($rev){" r$rev"})" } +if ($resComun.Commit -eq 'commit') { $commits += "COMUN: sync SVN$(if($revComun){" r$revComun"})" } $closedAll = @($brProj.Closed + $brComun.Closed) $activeAll = @($brProj.Active + $brComun.Active) +$warnAll = @($resProj.Warnings + $resComun.Warnings) Write-Host '' Write-Host "===================== SUMAR =====================" @@ -187,7 +320,13 @@ if ($commits.Count -gt 0) { } else { Write-Host "Commit-uri de sync: nimic de sincronizat" } +Write-Host "Origin: proiect=$($resProj.Origin)/push=$($resProj.Push) COMUN=$($resComun.Origin)/push=$($resComun.Push)" Write-Host "Branch-uri inchise: $(if($closedAll.Count){$closedAll -join ', '}else{'niciunul'})" Write-Host "Branch-uri active: $(if($activeAll.Count){$activeAll -join ', '}else{'niciunul'})" +if ($warnAll.Count -gt 0) { + Write-Host "AVERTISMENTE:" + $warnAll | ForEach-Object { Write-Host " $_" } +} Write-Host "================================================" +if (($resProj.Origin -eq 'conflict') -or ($resComun.Origin -eq 'conflict')) { exit 1 } exit 0 diff --git a/test_roa_sync.ps1 b/test_roa_sync.ps1 new file mode 100644 index 0000000..f6061d9 --- /dev/null +++ b/test_roa_sync.ps1 @@ -0,0 +1,239 @@ +<# +.SYNOPSIS + Teste standalone (fara Pester) pentru reconcilierea cu origin din roa_sync.ps1, pe + fixture-uri git construite in TEMP (origin bare + doua clone "masini"). Ruleaza + roa_sync.ps1 -GitOnly (fara SVN, fara FoxBin2Prg), deci nu atinge niciun proiect real. + + Scenarii: + (A) scenariul din 2026-07-27: modificari locale necomise + fisier netracked, toate + byte-identice cu ce a impins deja alta masina -> reconciliere automata (sync-ul + local devine gol la rebase si e aruncat), fara interventie manuala, exit 0; + (B) behind + continut local NOU -> rebase pastreaza sync-ul si il impinge pe origin; + (C) doar behind, fara modificari locale -> fast-forward la origin/main; + (D) conflict real (acelasi fisier, continut diferit) -> rebase --abort, exit nenul, + repo lasat curat (fara rebase in curs), commit-ul local pastrat; + (E) garda de push: commit non-sync ahead de origin -> push blocat + avertisment; + (F) fetch esuat (origin inaccesibil) -> comportamentul vechi (doar commit local), exit 0; + (G) push respins o data (hook pre-receive, simuleaza push simultan) -> retry reuseste; + (H) repo fara remote origin -> doar commit local, exit 0. +#> +[CmdletBinding()] +param() + +$ErrorActionPreference = 'Stop' +$script:RoaSync = 'D:\ROA\UTIL\foxbin2prg\roa_sync.ps1' +$script:Failures = @() + +function Assert([bool]$cond, [string]$msg) { + if ($cond) { Write-Host " PASS: $msg" -ForegroundColor Green } + else { Write-Host " FAIL: $msg" -ForegroundColor Red; $script:Failures += $msg } +} + +# --- git in fixture: EAP Continue local (git scrie pe stderr; sub Stop 2>$null ar arunca) --- +function G { + param([Parameter(Mandatory)][string]$Repo, + [Parameter(Mandatory, ValueFromRemainingArguments)][string[]]$GitArgs) + $prev = $ErrorActionPreference; $ErrorActionPreference = 'Continue' + $out = (& git -C $Repo @GitArgs 2>$null | Out-String).Trim() + $code = $LASTEXITCODE + $ErrorActionPreference = $prev + if ($code -ne 0) { throw "git -C $Repo $($GitArgs -join ' ') a esuat (exit $code)" } + return $out +} + +# scriere fisier cu octeti determinati (acelasi continut => aceiasi octeti pe ambele "masini") +function WF([string]$path, [string]$content) { [IO.File]::WriteAllText($path, $content) } + +function Set-RepoConfig([string]$repo) { + $null = G $repo config user.name 'Test ROA' + $null = G $repo config user.email 'test@roa.local' + $null = G $repo config commit.gpgsign false + $null = G $repo config core.autocrlf false +} + +# --- lume de test: origin bare + masinaA (a impins deja) + masinaB (fixture-ul testat) --- +function New-World([string]$tag) { + $root = Join-Path $env:TEMP ("rs_test_{0}_{1}_{2}" -f $tag, $PID, [Guid]::NewGuid().ToString('N').Substring(0,8)) + New-Item -ItemType Directory -Force -Path $root | Out-Null + $bare = Join-Path $root 'origin.git' + $null = G $root init --bare origin.git + $null = G $bare symbolic-ref HEAD refs/heads/main + # -c la clone seteaza configul in noul repo INAINTE de checkout: cu autocrlf global true, + # checkout-ul ar scrie CRLF si un add -A ulterior ar vedea fals toate fisierele modificate + $null = G $root clone -c core.autocrlf=false origin.git masinaA + $A = Join-Path $root 'masinaA' + Set-RepoConfig $A + $null = G $A checkout -b main + WF (Join-Path $A 'a.prg') "linie 1`nv1`n" + WF (Join-Path $A 'b.prg') "b v1`n" + $null = G $A add -A + $null = G $A commit -m 'initial' + $null = G $A push -u origin main + $null = G $root clone -c core.autocrlf=false origin.git masinaB + $B = Join-Path $root 'masinaB' + Set-RepoConfig $B + return [PSCustomObject]@{ Root=$root; Bare=$bare; A=$A; B=$B } +} + +function Remove-World($w) { + Remove-Item -Recurse -Force -LiteralPath $w.Root -ErrorAction SilentlyContinue +} + +function Invoke-RoaSync([string]$Fixture) { + $prev = $ErrorActionPreference; $ErrorActionPreference = 'Continue' + $raw = & powershell -ExecutionPolicy Bypass -NoProfile -File $script:RoaSync -ProjectRoot $Fixture -GitOnly 2>&1 | Out-String + $exit = $LASTEXITCODE + $ErrorActionPreference = $prev + return [PSCustomObject]@{ Exit=$exit; Raw=$raw } +} + +# ============================== SCENARIILE ============================== + +function Test-ContinutIdentic { + Write-Host "`n--- (A) scenariul 2026-07-27: local byte-identic cu origin (modificat + netracked) ---" + $w = New-World 'ident' + try { + # masina A a facut svn update + sync + push inaintea noastra + WF (Join-Path $w.A 'a.prg') "linie 1`nv2`n" + WF (Join-Path $w.A 'wincrypt.h') "#define W 1`n" + $null = G $w.A add -A; $null = G $w.A commit -m 'sync SVN r100'; $null = G $w.A push origin main + # masina B: acelasi svn update + regenerare => aceleasi continuturi, necomise/netracked + WF (Join-Path $w.B 'a.prg') "linie 1`nv2`n" + WF (Join-Path $w.B 'wincrypt.h') "#define W 1`n" + $r = Invoke-RoaSync $w.B + Assert ($r.Exit -eq 0) "A: exit 0, fara interventie manuala (exit=$($r.Exit))" + Assert ((G $w.B rev-parse main) -eq (G $w.Bare rev-parse main)) "A: main local == origin/main dupa reconciliere" + Assert ((G $w.B log -1 --format=%s) -eq 'sync SVN r100') "A: sync-ul local gol a fost aruncat (tip = commit-ul masinii A)" + Assert ([string]::IsNullOrWhiteSpace((G $w.B status --porcelain))) "A: working tree curat" + } finally { Remove-World $w } +} + +function Test-ContinutNou { + Write-Host "`n--- (B) behind + continut local nou: rebase pastreaza sync-ul si il impinge ---" + $w = New-World 'nou' + try { + WF (Join-Path $w.A 'a.prg') "linie 1`nv2`n" + $null = G $w.A add -A; $null = G $w.A commit -m 'sync SVN r100'; $null = G $w.A push origin main + # masina B: aceeasi modificare (identica) + un fisier NOU pe care origin nu il are + WF (Join-Path $w.B 'a.prg') "linie 1`nv2`n" + WF (Join-Path $w.B 'c.prg') "continut nou local`n" + $r = Invoke-RoaSync $w.B + Assert ($r.Exit -eq 0) "B: exit 0 (exit=$($r.Exit))" + Assert ((G $w.B log -1 --format=%s) -eq 'sync SVN') "B: sync-ul local (continut nou) pastrat dupa rebase" + Assert ((G $w.B rev-parse main) -eq (G $w.Bare rev-parse main)) "B: push automat - origin/main actualizat" + Assert ((G $w.Bare ls-tree --name-only main) -match 'c\.prg') "B: c.prg a ajuns pe origin" + } finally { Remove-World $w } +} + +function Test-DoarBehind { + Write-Host "`n--- (C) doar behind, fara modificari locale: fast-forward ---" + $w = New-World 'ff' + try { + WF (Join-Path $w.A 'b.prg') "b v2`n" + $null = G $w.A add -A; $null = G $w.A commit -m 'sync SVN r101'; $null = G $w.A push origin main + $r = Invoke-RoaSync $w.B + Assert ($r.Exit -eq 0) "C: exit 0 (exit=$($r.Exit))" + Assert ($r.Raw -match 'fast-forward') "C: reconciliere prin fast-forward" + Assert ((G $w.B rev-parse main) -eq (G $w.Bare rev-parse main)) "C: main local == origin/main" + } finally { Remove-World $w } +} + +function Test-ConflictReal { + Write-Host "`n--- (D) conflict real: rebase abortat, exit nenul, repo curat ---" + $w = New-World 'conf' + try { + WF (Join-Path $w.A 'a.prg') "linie 1`nvarianta A`n" + $null = G $w.A add -A; $null = G $w.A commit -m 'sync SVN r102'; $null = G $w.A push origin main + WF (Join-Path $w.B 'a.prg') "linie 1`nvarianta B`n" + $r = Invoke-RoaSync $w.B + Assert ($r.Exit -ne 0) "D: exit nenul la conflict real (exit=$($r.Exit))" + Assert ($r.Raw -match 'CONFLICT') "D: conflictul e raportat explicit" + $inRebase = (Test-Path (Join-Path $w.B '.git\rebase-merge')) -or (Test-Path (Join-Path $w.B '.git\rebase-apply')) + Assert (-not $inRebase) "D: rebase anulat curat (fara rebase in curs)" + Assert ((G $w.B log -1 --format=%s) -eq 'sync SVN') "D: commit-ul local de sync pastrat" + Assert ([string]::IsNullOrWhiteSpace((G $w.B status --porcelain))) "D: working tree curat dupa abort" + Assert ((G $w.Bare log -1 --format=%s) -eq 'sync SVN r102') "D: origin neatins" + } finally { Remove-World $w } +} + +function Test-GardaNonSync { + Write-Host "`n--- (E) garda de push: commit non-sync ahead blocheaza push-ul automat ---" + $w = New-World 'guard' + try { + $init = G $w.Bare rev-parse main + # commit local non-sync (lucrare in curs, nepushed) + modificari de sync necomise + WF (Join-Path $w.B 'd.prg') "lucru manual`n" + $null = G $w.B add -A; $null = G $w.B commit -m 'lucrare manuala neaprobata' + WF (Join-Path $w.B 'b.prg') "b v2`n" + $r = Invoke-RoaSync $w.B + Assert ($r.Exit -eq 0) "E: exit 0 (exit=$($r.Exit))" + Assert ($r.Raw -match 'non-sync') "E: avertisment despre commit-uri non-sync" + Assert ((G $w.Bare rev-parse main) -eq $init) "E: origin neatins (fara push)" + Assert ((G $w.B log -1 --format=%s) -eq 'sync SVN') "E: sync-ul local totusi comis" + } finally { Remove-World $w } +} + +function Test-Offline { + Write-Host "`n--- (F) fetch esuat (origin inaccesibil): doar commit local, exit 0 ---" + $w = New-World 'off' + try { + $null = G $w.B remote set-url origin (Join-Path $w.Root 'inexistent.git') + WF (Join-Path $w.B 'b.prg') "b v3`n" + $r = Invoke-RoaSync $w.B + Assert ($r.Exit -eq 0) "F: exit 0 (exit=$($r.Exit))" + Assert ($r.Raw -match 'fetch origin a esuat') "F: avertisment de fetch esuat" + Assert ((G $w.B log -1 --format=%s) -eq 'sync SVN') "F: commit-ul local de sync facut totusi" + } finally { Remove-World $w } +} + +function Test-PushRetry { + Write-Host "`n--- (G) push respins o data (simulare push simultan): retry reuseste ---" + $w = New-World 'retry' + try { + # hook pre-receive: respinge cat timp exista flagul reject_once, apoi accepta + $hook = "#!/bin/sh`nif [ -f reject_once ]; then`n rm -f reject_once`n exit 1`nfi`nexit 0`n" + [IO.File]::WriteAllText((Join-Path $w.Bare 'hooks\pre-receive'), $hook) + WF (Join-Path $w.Bare 'reject_once') "1" + WF (Join-Path $w.B 'b.prg') "b v4`n" + $r = Invoke-RoaSync $w.B + Assert ($r.Exit -eq 0) "G: exit 0 (exit=$($r.Exit))" + Assert ($r.Raw -match 'push respins') "G: prima incercare respinsa si raportata" + Assert ((G $w.Bare rev-parse main) -eq (G $w.B rev-parse main)) "G: push reusit la reincercare" + } finally { Remove-World $w } +} + +function Test-FaraOrigin { + Write-Host "`n--- (H) repo fara remote origin: doar commit local ---" + $w = New-World 'noorig' + try { + $null = G $w.B remote remove origin + WF (Join-Path $w.B 'b.prg') "b v5`n" + $r = Invoke-RoaSync $w.B + Assert ($r.Exit -eq 0) "H: exit 0 (exit=$($r.Exit))" + Assert ($r.Raw -match 'fara remote origin') "H: lipsa origin raportata" + Assert ((G $w.B log -1 --format=%s) -eq 'sync SVN') "H: commit-ul local de sync facut" + } finally { Remove-World $w } +} + +# ============================== MAIN ============================== +if (-not (Test-Path -LiteralPath $script:RoaSync)) { throw "Nu gasesc roa_sync.ps1: $script:RoaSync" } + +Write-Host "=== test_roa_sync.ps1 ===" +Test-ContinutIdentic +Test-ContinutNou +Test-DoarBehind +Test-ConflictReal +Test-GardaNonSync +Test-Offline +Test-PushRetry +Test-FaraOrigin + +Write-Host "`n=== REZULTAT ===" +if ($script:Failures.Count -eq 0) { + Write-Host "TOATE testele au trecut." -ForegroundColor Green + exit 0 +} else { + Write-Host "$($script:Failures.Count) test(e) esuate:" -ForegroundColor Red + $script:Failures | ForEach-Object { Write-Host " - $_" -ForegroundColor Red } + exit 1 +}