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<rev>], 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AvccPZXr4A9ihcTtExNoJT
This commit is contained in:
2026-07-27 11:52:23 +03:00
parent 558973f0c6
commit 51cab198c4
2 changed files with 430 additions and 52 deletions

View File

@@ -7,19 +7,44 @@
1. svn update pe ProjectRoot (include externalul COMUN\). Esec / conflicte -> stop, exit nenul.
2. git_sync.ps1 -ProjectRoot <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<REV>" 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<REV>" 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<rev>]" - 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<rev>".
# Intoarce: 'commit' / 'nimic' / 'skip-branch'.
# =========================== 3-4. commit sync + reconciliere origin per repo ===========================
# Pe main comite schimbarile ca "sync SVN r<rev>", 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

239
test_roa_sync.ps1 Normal file
View File

@@ -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
}