mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-22 04:31:02 +03:00
feat(cfe-patch-method): батч -Check/-Actualize контролируемых методов (v2.2)
После обновления КФ контролируемые методы (&ИзменениеИКонтроль) молча уезжают в рассинхрон — платформа при загрузке не ругается, ошибка лишь в рантайме. Добавлены два явных режима по всему расширению (или -ModulePath/-MethodName для сужения): - -Check — отчёт: какие методы дрейфнули (ДРЕЙФ/КОНФЛИКТ/МЕТОД-ИСЧЕЗ), актуальные числом; ничего не пишет; exit 1 при наличии дрейфа. - -Actualize — чинит пачкой: авто-перенос + merge-воркспейс на конфликтах. Одиночный ресинк вынесен в общую функцию resync_one (report_only), одиночный путь и батч используют её. Зона ответственности узкая — только тело &ИзменениеИКонтроль. Merge-воркспейс переработан: тонкий index.md (список конфликтов + пути к .bsl расширения) + подпапка на метод (conflict.md с блоком/диффом + base/local/remote), без общей портянки. cfe-validate: крошка-указатель [INFO] при наличии контролируемых методов -> /cfe-patch-method -Check. Тесты: +check-clean/check-drift/actualize-batch. 16 кейсов, оба рантайма зелёные, байтовый паритет ps1<->py. verify-snapshots (реальная 1С) Windows ps+py — 16/16. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8399f13e5a
commit
9aaa3a1c1e
@@ -1,4 +1,4 @@
|
||||
# cfe-patch-method v2.1 — Source-aware method interceptor for 1C extension (CFE)
|
||||
# cfe-patch-method v2.2 — Source-aware method interceptor for 1C extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -6,15 +6,17 @@ param(
|
||||
|
||||
[string]$ConfigPath,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
# Generation: logical name or path to .bsl. Optional in -Check/-Actualize (scope narrowing).
|
||||
[string]$ModulePath,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]$MethodName,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateSet("Before","After","Instead","ModificationAndControl")]
|
||||
[string]$InterceptorType
|
||||
[ValidateSet("", "Before", "After", "Instead", "ModificationAndControl")]
|
||||
[string]$InterceptorType,
|
||||
|
||||
# Batch modes over &ИзменениеИКонтроль of the extension (no generation):
|
||||
[switch]$Check, # report drift only, no writes
|
||||
[switch]$Actualize # actualize drifted controlled methods
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
@@ -475,6 +477,199 @@ function Get-Truncated {
|
||||
return $t
|
||||
}
|
||||
|
||||
# Reverse of typeDirMap (plural dir -> singular type) for logical id display
|
||||
$script:dirToType = @{
|
||||
"Catalogs"="Catalog"; "Documents"="Document"; "Enums"="Enum"; "CommonModules"="CommonModule"
|
||||
"Reports"="Report"; "DataProcessors"="DataProcessor"; "ExchangePlans"="ExchangePlan"
|
||||
"ChartsOfAccounts"="ChartOfAccounts"; "ChartsOfCharacteristicTypes"="ChartOfCharacteristicTypes"
|
||||
"ChartsOfCalculationTypes"="ChartOfCalculationTypes"; "BusinessProcesses"="BusinessProcess"
|
||||
"Tasks"="Task"; "InformationRegisters"="InformationRegister"
|
||||
"AccumulationRegisters"="AccumulationRegister"; "AccountingRegisters"="AccountingRegister"
|
||||
"CalculationRegisters"="CalculationRegister"
|
||||
}
|
||||
|
||||
# Rel-path segments (relative to ExtensionPath) -> logical ModulePath (Type.Name.Module / CommonModule.Name)
|
||||
function Get-ModulePathFromRel {
|
||||
param($relParts)
|
||||
$dir0 = $relParts[0]; $name = $relParts[1]
|
||||
$typ = if ($script:dirToType.ContainsKey($dir0)) { $script:dirToType[$dir0] } else { $dir0 }
|
||||
if ($dir0 -eq "CommonModules") { return "CommonModule.$name" }
|
||||
if ($relParts.Count -ge 7 -and $relParts[2] -eq "Forms") { return "$typ.$name.Form.$($relParts[3])" }
|
||||
$mod = $relParts[$relParts.Count - 1]
|
||||
if ($mod.EndsWith(".bsl")) { $mod = $mod.Substring(0, $mod.Length - 4) }
|
||||
return "$typ.$name.$mod"
|
||||
}
|
||||
|
||||
function Get-RelPartsUnder {
|
||||
param([string]$root, [string]$fullPath)
|
||||
$r = [IO.Path]::GetFullPath($root).TrimEnd('\', '/')
|
||||
$f = [IO.Path]::GetFullPath($fullPath)
|
||||
$rel = $f.Substring($r.Length).TrimStart('\', '/')
|
||||
return (($rel -replace '\\', '/').Split('/') | Where-Object { $_ -ne '' })
|
||||
}
|
||||
|
||||
function Get-ResyncConflictReason {
|
||||
param($disputed)
|
||||
$kinds = @($disputed | ForEach-Object { $_.Kind } | Select-Object -Unique)
|
||||
$parts = @()
|
||||
if ($kinds -contains 'insert') { $parts += 'якорь вставки изменён' }
|
||||
if ($kinds -contains 'delete') { $parts += 'удаляемое исчезло' }
|
||||
return ($parts -join '; ')
|
||||
}
|
||||
|
||||
# Write per-method conflict folder: conflict.md + base/local/remote
|
||||
function Write-ConflictFolder {
|
||||
param($folder, $methodId, $extBsl, $existingName, $method, $v1, $markedBody, $v2, $v1norm, $v2norm, $disputed, $enc)
|
||||
if (-not (Test-Path $folder)) { New-Item -ItemType Directory -Path $folder -Force | Out-Null }
|
||||
[IO.File]::WriteAllText((Join-Path $folder 'base.bsl'), (($v1 -join "`r`n") + "`r`n"), $enc)
|
||||
[IO.File]::WriteAllText((Join-Path $folder 'local.bsl'), (($markedBody -join "`r`n") + "`r`n"), $enc)
|
||||
[IO.File]::WriteAllText((Join-Path $folder 'remote.bsl'), (($v2 -join "`r`n") + "`r`n"), $enc)
|
||||
$md = @()
|
||||
$md += "# $methodId"
|
||||
$md += "Править: $extBsl"
|
||||
$md += "Метод: $existingName (&ИзменениеИКонтроль(`"$($method.Canonical)`"))"
|
||||
$md += "Причина: $(Get-ResyncConflictReason $disputed)"
|
||||
$md += ""
|
||||
$md += "## Не размещено — перенести в метод вручную"
|
||||
foreach ($d in $disputed) {
|
||||
if ($d.Kind -eq 'insert') { $md += '#Вставка'; foreach ($l in $d.Lines) { $md += $l }; $md += '#КонецВставки' }
|
||||
else { $md += '// не удалось найти для удаления:'; foreach ($l in $d.Lines) { $md += ('// ' + $l.Trim()) } }
|
||||
}
|
||||
$md += ""
|
||||
$md += "## Дифф base→remote (что изменилось в оригинале)"
|
||||
foreach ($l in $v1) { if ($v2norm -notcontains (Get-Normalized $l)) { $md += "- $l" } }
|
||||
foreach ($l in $v2) { if ($v1norm -notcontains (Get-Normalized $l)) { $md += "+ $l" } }
|
||||
$md += ""
|
||||
$md += "Рядом: base.bsl / local.bsl / remote.bsl"
|
||||
[IO.File]::WriteAllText((Join-Path $folder 'conflict.md'), (($md -join "`r`n") + "`r`n"), $enc)
|
||||
}
|
||||
|
||||
# Resync one &ИзменениеИКонтроль interceptor. Returns status hashtable.
|
||||
# ReportOnly: classify without writing. Otherwise: apply to module (+ conflict folder on disputes).
|
||||
function Invoke-Resync {
|
||||
param($extBsl, $extLines, $dup, $method, [string]$logicalModule, [string]$conflictFolder, [switch]$ReportOnly, $enc)
|
||||
|
||||
$methodId = "$logicalModule.$($method.Canonical)"
|
||||
$decLine = $dup.Line
|
||||
$sigLineIdx = $decLine + 1
|
||||
if ($sigLineIdx -ge $extLines.Count -or $extLines[$sigLineIdx] -notmatch '^\s*(?:Асинх\s+)?(?:Процедура|Функция)\s+([\w]+)\s*\(') {
|
||||
return @{ Id = $methodId; Status = 'ОШИБКА'; ExtBsl = $extBsl; Reason = 'не разобрать сигнатуру перехватчика' }
|
||||
}
|
||||
$existingName = $Matches[1]
|
||||
$sig = Read-Signature $extLines $sigLineIdx
|
||||
if (-not $sig) { return @{ Id = $methodId; Status = 'ОШИБКА'; ExtBsl = $extBsl; Reason = 'не разобрать сигнатуру' } }
|
||||
$sigEnd = $sig.EndLineIdx
|
||||
$isFunc = ($extLines[$sigLineIdx] -imatch '^\s*(?:Асинх\s+)?Функция\b')
|
||||
$endRe = if ($isFunc) { '^\s*КонецФункции\b' } else { '^\s*КонецПроцедуры\b' }
|
||||
$blockEnd = -1
|
||||
for ($j = $sigEnd + 1; $j -lt $extLines.Count; $j++) { if ($extLines[$j] -imatch $endRe) { $blockEnd = $j; break } }
|
||||
if ($blockEnd -lt 0) { return @{ Id = $methodId; Status = 'ОШИБКА'; ExtBsl = $extBsl; Reason = 'не найден конец перехватчика' } }
|
||||
|
||||
$markedBody = @()
|
||||
for ($j = $sigEnd + 1; $j -lt $blockEnd; $j++) { $markedBody += $extLines[$j] }
|
||||
|
||||
$parsed = Parse-MarkedBody $markedBody
|
||||
$v1 = $parsed.V1
|
||||
$ops = $parsed.Ops
|
||||
$v2 = $method.BodyLines
|
||||
$v1norm = @($v1 | ForEach-Object { Get-Normalized $_ })
|
||||
$v2norm = @($v2 | ForEach-Object { Get-Normalized $_ })
|
||||
|
||||
if (($v1norm -join "`n") -eq ($v2norm -join "`n")) {
|
||||
return @{ Id = $methodId; Status = 'АКТУАЛЕН'; ExtBsl = $extBsl }
|
||||
}
|
||||
|
||||
$insertTop = @(); $insertAfter = @{}; $delStart = @{}; $delEnd = @{}; $disputed = @(); $transferred = @()
|
||||
foreach ($op in $ops) {
|
||||
if ($op.Kind -eq 'insert') {
|
||||
$beforeLines = @()
|
||||
if ($op.After -ge 0) { $bs = [Math]::Max(0, $op.After - 2); for ($m = $bs; $m -le $op.After; $m++) { $beforeLines += $v1norm[$m] } }
|
||||
$afterLines = @()
|
||||
$ae = [Math]::Min($v1norm.Count - 1, $op.After + 3)
|
||||
for ($m = $op.After + 1; $m -le $ae; $m++) { $afterLines += $v1norm[$m] }
|
||||
$k = Resolve-InsertionPoint $v2norm $beforeLines $afterLines
|
||||
if ($null -eq $k) { $disputed += @{ Kind = 'insert'; Lines = $op.Lines } }
|
||||
elseif ($k -lt 0) { $insertTop += ,$op.Lines; $transferred += @{ Kind = 'insert' } }
|
||||
else { if (-not $insertAfter.ContainsKey($k)) { $insertAfter[$k] = @() }; $insertAfter[$k] += ,$op.Lines; $transferred += @{ Kind = 'insert' } }
|
||||
} else {
|
||||
$keys = @(); for ($m = $op.Start; $m -le $op.End; $m++) { $keys += $v1norm[$m] }
|
||||
$p = Find-UniqueRun $v2norm $keys
|
||||
if ($p -ge 0) { $delStart[$p] = $true; $delEnd[$p + $keys.Count - 1] = $true; $transferred += @{ Kind = 'delete' } }
|
||||
else { $disputed += @{ Kind = 'delete'; Lines = $op.Lines } }
|
||||
}
|
||||
}
|
||||
|
||||
if ($ReportOnly) {
|
||||
$st = if ($disputed.Count -gt 0) { 'КОНФЛИКТ' } else { 'ДРЕЙФ' }
|
||||
return @{ Id = $methodId; Status = $st; ExtBsl = $extBsl; Transferred = $transferred.Count; Disputed = $disputed.Count; Reason = (Get-ResyncConflictReason $disputed) }
|
||||
}
|
||||
|
||||
# assemble new marked body
|
||||
$newBody = @()
|
||||
foreach ($blk in $insertTop) { $newBody += "#Вставка"; foreach ($l in $blk) { $newBody += $l }; $newBody += "#КонецВставки" }
|
||||
for ($k = 0; $k -lt $v2.Count; $k++) {
|
||||
if ($delStart.ContainsKey($k)) { $newBody += "#Удаление" }
|
||||
$newBody += $v2[$k]
|
||||
if ($delEnd.ContainsKey($k)) { $newBody += "#КонецУдаления" }
|
||||
if ($insertAfter.ContainsKey($k)) { foreach ($blk in $insertAfter[$k]) { $newBody += "#Вставка"; foreach ($l in $blk) { $newBody += $l }; $newBody += "#КонецВставки" } }
|
||||
}
|
||||
if ($disputed.Count -gt 0) {
|
||||
$newBody += "`t// [РЕСИНК-КОНФЛИКТ] перенесите блоки ниже вручную — исходный якорь изменился в новой версии оригинала."
|
||||
$newBody += "`t// Материалы: см. index.md / conflict.md в merge-воркспейсе (путь в выводе)."
|
||||
foreach ($d in $disputed) {
|
||||
if ($d.Kind -eq 'insert') { $newBody += "#Вставка"; foreach ($l in $d.Lines) { $newBody += $l }; $newBody += "#КонецВставки" }
|
||||
else { $newBody += "`t// [РЕСИНК-КОНФЛИКТ] не удалось найти для удаления:"; foreach ($l in $d.Lines) { $newBody += ("`t// " + $l.Trim()) } }
|
||||
}
|
||||
}
|
||||
$asyncPrefix = if ($method.IsAsync) { "Асинх " } else { "" }
|
||||
$keyword = if ($method.IsFunction) { "Функция" } else { "Процедура" }
|
||||
$endKeyword = if ($method.IsFunction) { "КонецФункции" } else { "КонецПроцедуры" }
|
||||
$newBlock = @()
|
||||
if ($method.Context) { $newBlock += $method.Context }
|
||||
$newBlock += "&ИзменениеИКонтроль(`"$($method.Canonical)`")"
|
||||
$newBlock += "$asyncPrefix$keyword $existingName($($method.ParamsText))"
|
||||
$newBlock += $newBody
|
||||
$newBlock += $endKeyword
|
||||
|
||||
$blockStart = $decLine
|
||||
if ($decLine -ge 1 -and (Test-ContextDirective $extLines[$decLine - 1].Trim())) { $blockStart = $decLine - 1 }
|
||||
$out = @()
|
||||
for ($j = 0; $j -lt $blockStart; $j++) { $out += $extLines[$j] }
|
||||
foreach ($l in $newBlock) { $out += $l }
|
||||
for ($j = $blockEnd + 1; $j -lt $extLines.Count; $j++) { $out += $extLines[$j] }
|
||||
[IO.File]::WriteAllText($extBsl, (($out -join "`r`n") + "`r`n"), $enc)
|
||||
|
||||
$conflictDir = $null
|
||||
if ($disputed.Count -gt 0) {
|
||||
$conflictDir = $conflictFolder
|
||||
Write-ConflictFolder $conflictFolder $methodId $extBsl $existingName $method $v1 $markedBody $v2 $v1norm $v2norm $disputed $enc
|
||||
}
|
||||
$status = if ($disputed.Count -gt 0) { 'ЧАСТИЧНО' } else { 'АКТУАЛИЗИРОВАН' }
|
||||
return @{ Id = $methodId; Status = $status; ExtBsl = $extBsl; Transferred = $transferred.Count; Disputed = $disputed.Count; ConflictDir = $conflictDir; Reason = (Get-ResyncConflictReason $disputed) }
|
||||
}
|
||||
|
||||
# Write run-root index.md (only if there are conflicts). Returns run-root or $null.
|
||||
function Write-ResyncIndex {
|
||||
param($runRoot, $results, [string]$extName, [string]$configPath, [string]$verb, $enc)
|
||||
$conflicts = @($results | Where-Object { $_.Status -eq 'ЧАСТИЧНО' })
|
||||
if ($conflicts.Count -eq 0) { return $null }
|
||||
if (-not (Test-Path $runRoot)) { New-Item -ItemType Directory -Path $runRoot -Force | Out-Null }
|
||||
$total = $results.Count
|
||||
$actual = @($results | Where-Object { $_.Status -eq 'АКТУАЛЕН' }).Count
|
||||
$upd = @($results | Where-Object { $_.Status -eq 'АКТУАЛИЗИРОВАН' }).Count
|
||||
$lines = @()
|
||||
$lines += "[$verb] $extName -> $configPath"
|
||||
$lines += "Итог: $actual/$total актуальны · $upd актуализировано · $($conflicts.Count) конфликтов"
|
||||
$lines += ""
|
||||
$lines += "Конфликты — править .bsl расширения:"
|
||||
foreach ($c in $conflicts) {
|
||||
$lines += " ЧАСТИЧНО $($c.Id)"
|
||||
$lines += " -> $($c.ExtBsl) ($($c.Reason))"
|
||||
}
|
||||
[IO.File]::WriteAllText((Join-Path $runRoot 'index.md'), (($lines -join "`r`n") + "`r`n"), $enc)
|
||||
return $runRoot
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Main
|
||||
# ============================================================================
|
||||
@@ -494,6 +689,93 @@ $cfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$propsNode = $cfgDoc.SelectSingleNode("//md:Configuration/md:Properties", $cfgNs)
|
||||
$prefixNode = if ($propsNode) { $propsNode.SelectSingleNode("md:NamePrefix", $cfgNs) } else { $null }
|
||||
$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "Расш_" }
|
||||
$extNameNode = if ($propsNode) { $propsNode.SelectSingleNode("md:Name", $cfgNs) } else { $null }
|
||||
$extName = if ($extNameNode -and $extNameNode.InnerText) { $extNameNode.InnerText } else { "Расширение" }
|
||||
|
||||
# ============================================================================
|
||||
# Batch modes: -Check / -Actualize over &ИзменениеИКонтроль of the extension
|
||||
# ============================================================================
|
||||
if ($Check -or $Actualize) {
|
||||
if ($Check -and $Actualize) { Write-Error "Укажите либо -Check, либо -Actualize, не оба."; exit 1 }
|
||||
if ([string]::IsNullOrEmpty($ConfigPath)) { Write-Error "Для -Check/-Actualize нужен -ConfigPath (сверка с исходником)."; exit 1 }
|
||||
if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { $ConfigPath = Join-Path (Get-Location).Path $ConfigPath }
|
||||
if (Test-Path $ConfigPath -PathType Leaf) { $ConfigPath = Split-Path $ConfigPath -Parent }
|
||||
if (-not (Test-Path (Join-Path $ConfigPath "Configuration.xml"))) { Write-Error "Configuration.xml не найден в конфигурации-источнике: $ConfigPath"; exit 1 }
|
||||
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
$reportOnly = [bool]$Check
|
||||
$verb = if ($Check) { "КОНТРОЛЬ" } else { "АКТУАЛИЗАЦИЯ" }
|
||||
|
||||
# target .bsl set: -ModulePath → один модуль; иначе — все .bsl расширения
|
||||
$targetBsls = @()
|
||||
if (-not [string]::IsNullOrEmpty($ModulePath)) {
|
||||
$relParts = Get-ModuleRelPath $ModulePath
|
||||
$mb = $ExtensionPath; foreach ($p in $relParts) { $mb = Join-Path $mb $p }
|
||||
if (Test-Path $mb) { $targetBsls += $mb }
|
||||
} else {
|
||||
$targetBsls = @(Get-ChildItem -Path $ExtensionPath -Recurse -Filter *.bsl -File | ForEach-Object { $_.FullName })
|
||||
}
|
||||
|
||||
$runRoot = Join-Path ([IO.Path]::GetTempPath()) (Join-Path "cfe-resync" ($extName -replace '[\\/:*?"<>|]', '_'))
|
||||
if ($Actualize -and (Test-Path $runRoot)) { Remove-Item $runRoot -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
|
||||
$results = @()
|
||||
foreach ($tb in $targetBsls) {
|
||||
$scan = @([IO.File]::ReadAllLines($tb, [Text.Encoding]::UTF8))
|
||||
$mnames = @(Get-Interceptors $scan | Where-Object { $_.Type -eq 'ИзменениеИКонтроль' } | ForEach-Object { $_.Method })
|
||||
if (-not [string]::IsNullOrEmpty($MethodName)) { $mnames = @($mnames | Where-Object { $_ -ieq $MethodName }) }
|
||||
$mnames = @($mnames | Select-Object -Unique)
|
||||
if ($mnames.Count -eq 0) { continue }
|
||||
$rel = Get-RelPartsUnder $ExtensionPath $tb
|
||||
$logicalModule = Get-ModulePathFromRel $rel
|
||||
$srcBsl = $ConfigPath; foreach ($p in $rel) { $srcBsl = Join-Path $srcBsl $p }
|
||||
$relJoined = ($rel -join '\') -replace '\.bsl$', ''
|
||||
foreach ($mname in $mnames) {
|
||||
$mid = "$logicalModule.$mname"
|
||||
if (-not (Test-Path $srcBsl)) { $results += @{ Id = $mid; Status = 'ИСТОЧНИК-НЕ-НАЙДЕН'; ExtBsl = $tb }; continue }
|
||||
$srcL = [IO.File]::ReadAllLines($srcBsl, [Text.Encoding]::UTF8)
|
||||
$m = Extract-Method $srcL $mname
|
||||
if (-not $m) { $results += @{ Id = $mid; Status = 'МЕТОД-ИСЧЕЗ'; ExtBsl = $tb }; continue }
|
||||
# fresh read per method (writes shift line numbers)
|
||||
$tbLines = @([IO.File]::ReadAllLines($tb, [Text.Encoding]::UTF8))
|
||||
$ic = @(Get-Interceptors $tbLines | Where-Object { $_.Type -eq 'ИзменениеИКонтроль' -and $_.Method -ieq $mname })[0]
|
||||
if (-not $ic) { continue }
|
||||
$folder = Join-Path $runRoot (Join-Path $relJoined $m.Canonical)
|
||||
$results += (Invoke-Resync $tb $tbLines $ic $m $logicalModule $folder -ReportOnly:$reportOnly $enc)
|
||||
}
|
||||
}
|
||||
|
||||
# --- report ---
|
||||
$total = $results.Count
|
||||
$actual = @($results | Where-Object { $_.Status -eq 'АКТУАЛЕН' }).Count
|
||||
Write-Host "[$verb] $extName -> $ConfigPath (на контроле: $total)"
|
||||
$listed = @($results | Where-Object { $_.Status -ne 'АКТУАЛЕН' })
|
||||
foreach ($p in $listed) {
|
||||
$line = " {0,-16} {1}" -f $p.Status, $p.Id
|
||||
if ($p.Reason) { $line += " $($p.Reason)" }
|
||||
Write-Host $line
|
||||
}
|
||||
if ($Check) {
|
||||
$drift = @($results | Where-Object { $_.Status -eq 'ДРЕЙФ' }).Count
|
||||
$confl = @($results | Where-Object { $_.Status -eq 'КОНФЛИКТ' }).Count
|
||||
$gone = @($results | Where-Object { $_.Status -in @('МЕТОД-ИСЧЕЗ', 'ИСТОЧНИК-НЕ-НАЙДЕН') }).Count
|
||||
Write-Host "Итог: $actual/$total актуальны · $drift дрейф · $confl конфликт · $gone требуют внимания"
|
||||
if ($listed.Count -gt 0) { Write-Host "Починить: /cfe-patch-method -Actualize -ExtensionPath $ExtensionPath -ConfigPath $ConfigPath"; exit 1 } else { exit 0 }
|
||||
} else {
|
||||
$upd = @($results | Where-Object { $_.Status -eq 'АКТУАЛИЗИРОВАН' }).Count
|
||||
$part = @($results | Where-Object { $_.Status -eq 'ЧАСТИЧНО' }).Count
|
||||
Write-Host "Итог: $actual/$total актуальны · $upd актуализировано · $part частично"
|
||||
$idx = Write-ResyncIndex $runRoot $results $extName $ConfigPath $verb $enc
|
||||
if ($idx) { Write-Host "Merge-воркспейс конфликтов (см. index.md): $idx" }
|
||||
exit 0
|
||||
}
|
||||
}
|
||||
|
||||
# --- Generation mode: require ModulePath + MethodName + InterceptorType ---
|
||||
if ([string]::IsNullOrEmpty($ModulePath) -or [string]::IsNullOrEmpty($MethodName) -or [string]::IsNullOrEmpty($InterceptorType)) {
|
||||
Write-Error "Нужны -ModulePath, -MethodName, -InterceptorType (генерация перехватчика). Для проверки/актуализации контролируемых методов используйте -Check или -Actualize."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Resolve module file paths (ModulePath = logical name OR path to a .bsl) ---
|
||||
$hasConfigPath = -not [string]::IsNullOrEmpty($ConfigPath)
|
||||
@@ -568,171 +850,23 @@ if ($dup) {
|
||||
Write-Host " Файл: $extBsl"
|
||||
exit 0
|
||||
}
|
||||
# ---- RESYNC (&ИзменениеИКонтроль) ----
|
||||
# Locate existing block: decorator line -> signature -> body -> Конец*
|
||||
$decLine = $dup.Line
|
||||
# Interceptor procedure name from existing signature (line after decorator, skipping possible async)
|
||||
$sigLineIdx = $decLine + 1
|
||||
if ($sigLineIdx -ge $extLines.Count -or $extLines[$sigLineIdx] -notmatch '^\s*(?:Асинх\s+)?(?:Процедура|Функция)\s+([\w]+)\s*\(') {
|
||||
Write-Error "Не удалось найти сигнатуру существующего перехватчика &ИзменениеИКонтроль(`"$MethodName`")"; exit 1
|
||||
}
|
||||
$existingName = $Matches[1]
|
||||
$sig = Read-Signature $extLines $sigLineIdx
|
||||
if (-not $sig) { Write-Error "Не удалось разобрать сигнатуру существующего перехватчика"; exit 1 }
|
||||
$sigEnd = $sig.EndLineIdx
|
||||
$isFunc = ($extLines[$sigLineIdx] -imatch '^\s*(?:Асинх\s+)?Функция\b')
|
||||
$endRe = if ($isFunc) { '^\s*КонецФункции\b' } else { '^\s*КонецПроцедуры\b' }
|
||||
$blockEnd = -1
|
||||
for ($j = $sigEnd + 1; $j -lt $extLines.Count; $j++) { if ($extLines[$j] -imatch $endRe) { $blockEnd = $j; break } }
|
||||
if ($blockEnd -lt 0) { Write-Error "Не найден конец существующего перехватчика"; exit 1 }
|
||||
|
||||
# marked body
|
||||
$markedBody = @()
|
||||
for ($j = $sigEnd + 1; $j -lt $blockEnd; $j++) { $markedBody += $extLines[$j] }
|
||||
|
||||
# reconstruct v1, ops
|
||||
$parsed = Parse-MarkedBody $markedBody
|
||||
$v1 = $parsed.V1
|
||||
$ops = $parsed.Ops
|
||||
$v2 = $method.BodyLines
|
||||
|
||||
$v1norm = @($v1 | ForEach-Object { Get-Normalized $_ })
|
||||
$v2norm = @($v2 | ForEach-Object { Get-Normalized $_ })
|
||||
|
||||
# no drift?
|
||||
if (($v1norm -join "`n") -eq ($v2norm -join "`n")) {
|
||||
Write-Host "[АКТУАЛЕН] &ИзменениеИКонтроль(`"$MethodName`") — оригинал не менялся, изменений нет."
|
||||
Write-Host " Файл: $extBsl"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# transfer ops onto v2
|
||||
$insertTop = @()
|
||||
$insertAfter = @{} # v2 index -> list of blocks (each = @{Lines=..})
|
||||
$delStart = @{} # v2 index -> $true
|
||||
$delEnd = @{}
|
||||
$disputed = @()
|
||||
$transferred = @() # for the summary: @{ Kind; Anchor }
|
||||
|
||||
foreach ($op in $ops) {
|
||||
if ($op.Kind -eq 'insert') {
|
||||
# two-sided context anchor
|
||||
$beforeLines = @()
|
||||
if ($op.After -ge 0) {
|
||||
$bs = [Math]::Max(0, $op.After - 2)
|
||||
for ($m = $bs; $m -le $op.After; $m++) { $beforeLines += $v1norm[$m] }
|
||||
}
|
||||
$afterLines = @()
|
||||
$ae = [Math]::Min($v1norm.Count - 1, $op.After + 3)
|
||||
for ($m = $op.After + 1; $m -le $ae; $m++) { $afterLines += $v1norm[$m] }
|
||||
|
||||
$k = Resolve-InsertionPoint $v2norm $beforeLines $afterLines
|
||||
if ($null -eq $k) {
|
||||
$disputed += @{ Kind = 'insert'; Lines = $op.Lines }
|
||||
} elseif ($k -lt 0) {
|
||||
$insertTop += ,$op.Lines
|
||||
$transferred += @{ Kind = 'insert'; Anchor = '(в начало метода)' }
|
||||
} else {
|
||||
if (-not $insertAfter.ContainsKey($k)) { $insertAfter[$k] = @() }
|
||||
$insertAfter[$k] += ,$op.Lines
|
||||
$transferred += @{ Kind = 'insert'; Anchor = $v2[$k] }
|
||||
}
|
||||
} else {
|
||||
$keys = @()
|
||||
for ($m = $op.Start; $m -le $op.End; $m++) { $keys += $v1norm[$m] }
|
||||
$p = Find-UniqueRun $v2norm $keys
|
||||
if ($p -ge 0) {
|
||||
$delStart[$p] = $true
|
||||
$delEnd[$p + $keys.Count - 1] = $true
|
||||
$transferred += @{ Kind = 'delete'; Anchor = $op.Lines[0] }
|
||||
} else {
|
||||
$disputed += @{ Kind = 'delete'; Lines = $op.Lines }
|
||||
}
|
||||
# ---- RESYNC (&ИзменениеИКонтроль) — via Invoke-Resync ----
|
||||
$rel = Get-RelPartsUnder $ExtensionPath $extBsl
|
||||
$logicalModule = Get-ModulePathFromRel $rel
|
||||
$relJoined = ($rel -join '') -replace '.bsl$', ''
|
||||
$runRoot = Join-Path ([IO.Path]::GetTempPath()) (Join-Path "cfe-resync" ($extName -replace '[\/:*?"<>|]', '_'))
|
||||
$folder = Join-Path $runRoot (Join-Path $relJoined $method.Canonical)
|
||||
$res = Invoke-Resync $extBsl $extLines $dup $method $logicalModule $folder $enc
|
||||
switch ($res.Status) {
|
||||
'АКТУАЛЕН' { Write-Host "[АКТУАЛЕН] &ИзменениеИКонтроль(`"$MethodName`") — оригинал не менялся, изменений нет." }
|
||||
'АКТУАЛИЗИРОВАН' { Write-Host "[АКТУАЛИЗИРОВАН] &ИзменениеИКонтроль(`"$MethodName`") — тело обновлено, перенесено правок: $($res.Transferred)" }
|
||||
'ЧАСТИЧНО' {
|
||||
$idx = Write-ResyncIndex $runRoot @($res) $extName $ConfigPath "АКТУАЛИЗАЦИЯ" $enc
|
||||
Write-Host "[АКТУАЛИЗИРОВАН-ЧАСТИЧНО] &ИзменениеИКонтроль(`"$MethodName`") — перенесено: $($res.Transferred), конфликтов: $($res.Disputed)"
|
||||
Write-Host " Конфликт помечен // [РЕСИНК-КОНФЛИКТ]. Папка метода: $($res.ConflictDir)"
|
||||
if ($idx) { Write-Host " Индекс: $idx" }
|
||||
}
|
||||
}
|
||||
|
||||
# assemble new marked body
|
||||
$newBody = @()
|
||||
foreach ($blk in $insertTop) {
|
||||
$newBody += "#Вставка"; foreach ($l in $blk) { $newBody += $l }; $newBody += "#КонецВставки"
|
||||
}
|
||||
for ($k = 0; $k -lt $v2.Count; $k++) {
|
||||
if ($delStart.ContainsKey($k)) { $newBody += "#Удаление" }
|
||||
$newBody += $v2[$k]
|
||||
if ($delEnd.ContainsKey($k)) { $newBody += "#КонецУдаления" }
|
||||
if ($insertAfter.ContainsKey($k)) {
|
||||
foreach ($blk in $insertAfter[$k]) {
|
||||
$newBody += "#Вставка"; foreach ($l in $blk) { $newBody += $l }; $newBody += "#КонецВставки"
|
||||
}
|
||||
}
|
||||
}
|
||||
# disputed blocks appended with conflict markers (never lost)
|
||||
if ($disputed.Count -gt 0) {
|
||||
$newBody += "`t// [РЕСИНК-КОНФЛИКТ] перенесите блоки ниже вручную — исходный якорь изменился в новой версии оригинала."
|
||||
$newBody += "`t// Материалы для анализа см. в выводе команды (файлы base/local/remote/merged)."
|
||||
foreach ($d in $disputed) {
|
||||
if ($d.Kind -eq 'insert') {
|
||||
$newBody += "#Вставка"; foreach ($l in $d.Lines) { $newBody += $l }; $newBody += "#КонецВставки"
|
||||
} else {
|
||||
$newBody += "`t// [РЕСИНК-КОНФЛИКТ] не удалось найти для удаления:"
|
||||
foreach ($l in $d.Lines) { $newBody += ("`t// " + $l.Trim()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# rebuild block: keep context (from v2), decorator, signature with existing name and v2 params
|
||||
$asyncPrefix = if ($method.IsAsync) { "Асинх " } else { "" }
|
||||
$keyword = if ($method.IsFunction) { "Функция" } else { "Процедура" }
|
||||
$endKeyword = if ($method.IsFunction) { "КонецФункции" } else { "КонецПроцедуры" }
|
||||
$newBlock = @()
|
||||
if ($method.Context) { $newBlock += $method.Context }
|
||||
$newBlock += "&ИзменениеИКонтроль(`"$($method.Canonical)`")"
|
||||
$newBlock += "$asyncPrefix$keyword $existingName($($method.ParamsText))"
|
||||
$newBlock += $newBody
|
||||
$newBlock += $endKeyword
|
||||
|
||||
# block start includes preceding context directive line if present
|
||||
$blockStart = $decLine
|
||||
if ($decLine -ge 1 -and (Test-ContextDirective $extLines[$decLine - 1].Trim())) { $blockStart = $decLine - 1 }
|
||||
|
||||
$out = @()
|
||||
for ($j = 0; $j -lt $blockStart; $j++) { $out += $extLines[$j] }
|
||||
foreach ($l in $newBlock) { $out += $l }
|
||||
for ($j = $blockEnd + 1; $j -lt $extLines.Count; $j++) { $out += $extLines[$j] }
|
||||
|
||||
[System.IO.File]::WriteAllText($extBsl, (($out -join "`r`n") + "`r`n"), $enc)
|
||||
|
||||
# transferred summary (shared by full-auto and partial)
|
||||
function Write-TransferSummary {
|
||||
param($items)
|
||||
foreach ($t in $items) {
|
||||
if ($t.Kind -eq 'insert') { Write-Host " • вставка после «$(Get-Truncated $t.Anchor)»" }
|
||||
else { Write-Host " • удаление «$(Get-Truncated $t.Anchor)»" }
|
||||
}
|
||||
}
|
||||
|
||||
if ($disputed.Count -gt 0) {
|
||||
# merge workspace (git-mergetool convention: base/local/remote/merged)
|
||||
$tmpRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("cfe-resync\" + ($ModulePath -replace '[\\/:*?"<>|]', '_') + "." + $MethodName)
|
||||
if (-not (Test-Path $tmpRoot)) { New-Item -ItemType Directory -Path $tmpRoot -Force | Out-Null }
|
||||
[System.IO.File]::WriteAllText((Join-Path $tmpRoot "base.bsl"), (($v1 -join "`r`n") + "`r`n"), $enc)
|
||||
[System.IO.File]::WriteAllText((Join-Path $tmpRoot "local.bsl"), (($markedBody -join "`r`n") + "`r`n"), $enc)
|
||||
[System.IO.File]::WriteAllText((Join-Path $tmpRoot "remote.bsl"), (($v2 -join "`r`n") + "`r`n"), $enc)
|
||||
[System.IO.File]::WriteAllText((Join-Path $tmpRoot "merged.bsl"), (($newBlock -join "`r`n") + "`r`n"), $enc)
|
||||
$diff = @()
|
||||
$diff += "--- base -> remote (что изменилось в оригинале) ---"
|
||||
foreach ($l in $v1) { if ($v2norm -notcontains (Get-Normalized $l)) { $diff += "- $l" } }
|
||||
foreach ($l in $v2) { if ($v1norm -notcontains (Get-Normalized $l)) { $diff += "+ $l" } }
|
||||
[System.IO.File]::WriteAllText((Join-Path $tmpRoot "diff.txt"), (($diff -join "`r`n") + "`r`n"), $enc)
|
||||
|
||||
Write-Host "[АКТУАЛИЗИРОВАН-ЧАСТИЧНО] &ИзменениеИКонтроль(`"$MethodName`") — перенесено: $($transferred.Count), конфликтов: $($disputed.Count)"
|
||||
Write-TransferSummary $transferred
|
||||
Write-Host " Конфликтные блоки помечены // [РЕСИНК-КОНФЛИКТ] — разместите вручную."
|
||||
Write-Host " Merge-воркспейс (base/local/remote/merged/diff):"
|
||||
Write-Host " $tmpRoot"
|
||||
} else {
|
||||
Write-Host "[АКТУАЛИЗИРОВАН] &ИзменениеИКонтроль(`"$MethodName`") — тело обновлено, перенесено правок: $($transferred.Count)"
|
||||
Write-TransferSummary $transferred
|
||||
default { Write-Host "[$($res.Status)] &ИзменениеИКонтроль(`"$MethodName`") — $($res.Reason)" }
|
||||
}
|
||||
Write-Host " Файл: $extBsl"
|
||||
exit 0
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
# cfe-patch-method v2.1 — Source-aware method interceptor for 1C extension (CFE)
|
||||
# cfe-patch-method v2.2 — Source-aware method interceptor for 1C extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import xml.etree.ElementTree as ET
|
||||
@@ -475,10 +476,12 @@ def main():
|
||||
)
|
||||
parser.add_argument("-ExtensionPath", required=True)
|
||||
parser.add_argument("-ConfigPath", required=False, default="")
|
||||
parser.add_argument("-ModulePath", required=True)
|
||||
parser.add_argument("-MethodName", required=True)
|
||||
parser.add_argument("-InterceptorType", required=True,
|
||||
choices=["Before", "After", "Instead", "ModificationAndControl"])
|
||||
parser.add_argument("-ModulePath", required=False, default="")
|
||||
parser.add_argument("-MethodName", required=False, default="")
|
||||
parser.add_argument("-InterceptorType", required=False, default="",
|
||||
choices=["", "Before", "After", "Instead", "ModificationAndControl"])
|
||||
parser.add_argument("-Check", action="store_true")
|
||||
parser.add_argument("-Actualize", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
extension_path = args.ExtensionPath
|
||||
@@ -486,6 +489,8 @@ def main():
|
||||
module_path = args.ModulePath
|
||||
method_name = args.MethodName
|
||||
interceptor_type = args.InterceptorType
|
||||
check_mode = args.Check
|
||||
actualize_mode = args.Actualize
|
||||
|
||||
# --- Resolve extension path ---
|
||||
if not os.path.isabs(extension_path):
|
||||
@@ -506,8 +511,111 @@ def main():
|
||||
pn = props.find("md:NamePrefix", ns)
|
||||
if pn is not None and pn.text:
|
||||
name_prefix = pn.text
|
||||
nn = props.find("md:Name", ns)
|
||||
if nn is not None and nn.text:
|
||||
ext_name = nn.text
|
||||
except ET.ParseError:
|
||||
pass
|
||||
if 'ext_name' not in dir():
|
||||
ext_name = "Расширение"
|
||||
|
||||
# --- Batch modes: -Check / -Actualize over &ИзменениеИКонтроль ---
|
||||
if check_mode or actualize_mode:
|
||||
if check_mode and actualize_mode:
|
||||
die("Укажите либо -Check, либо -Actualize, не оба.")
|
||||
if not config_path:
|
||||
die("Для -Check/-Actualize нужен -ConfigPath (сверка с исходником).")
|
||||
cp = config_path
|
||||
if not os.path.isabs(cp):
|
||||
cp = os.path.join(os.getcwd(), cp)
|
||||
if os.path.isfile(cp):
|
||||
cp = os.path.dirname(cp)
|
||||
if not os.path.isfile(os.path.join(cp, "Configuration.xml")):
|
||||
die("Configuration.xml не найден в конфигурации-источнике: %s" % cp)
|
||||
report_only = check_mode
|
||||
verb = "КОНТРОЛЬ" if check_mode else "АКТУАЛИЗАЦИЯ"
|
||||
safe = re.sub(r'[\\/:*?"<>|]', "_", ext_name)
|
||||
run_root = os.path.join(tempfile.gettempdir(), "cfe-resync", safe)
|
||||
if actualize_mode and os.path.isdir(run_root):
|
||||
shutil.rmtree(run_root, ignore_errors=True)
|
||||
|
||||
targets = []
|
||||
if module_path:
|
||||
rp = get_module_rel_path(module_path)
|
||||
mb = os.path.join(extension_path, *rp)
|
||||
if os.path.isfile(mb):
|
||||
targets.append(mb)
|
||||
else:
|
||||
for dirpath, _dirs, files in os.walk(extension_path):
|
||||
for fn in files:
|
||||
if fn.endswith(".bsl"):
|
||||
targets.append(os.path.join(dirpath, fn))
|
||||
|
||||
results = []
|
||||
for tb in targets:
|
||||
scan = read_lines(tb)
|
||||
mnames = [ic["method"] for ic in get_interceptors(scan) if ic["type"] == "ИзменениеИКонтроль"]
|
||||
if method_name:
|
||||
mnames = [m for m in mnames if m.lower() == method_name.lower()]
|
||||
seen = set(); uniq = []
|
||||
for m in mnames:
|
||||
if m.lower() not in seen:
|
||||
seen.add(m.lower()); uniq.append(m)
|
||||
if not uniq:
|
||||
continue
|
||||
rel = rel_parts_under(extension_path, tb)
|
||||
logical_module = module_path_from_rel(rel)
|
||||
src_bsl2 = os.path.join(cp, *rel)
|
||||
rel_no_bsl = os.path.join(*rel)
|
||||
if rel_no_bsl.endswith(".bsl"):
|
||||
rel_no_bsl = rel_no_bsl[:-4]
|
||||
for mname in uniq:
|
||||
mid = "%s.%s" % (logical_module, mname)
|
||||
if not os.path.isfile(src_bsl2):
|
||||
results.append({"id": mid, "status": "ИСТОЧНИК-НЕ-НАЙДЕН", "ext_bsl": tb}); continue
|
||||
m = extract_method(read_lines(src_bsl2), mname)
|
||||
if not m:
|
||||
results.append({"id": mid, "status": "МЕТОД-ИСЧЕЗ", "ext_bsl": tb}); continue
|
||||
tb_lines = read_lines(tb)
|
||||
ic = next((x for x in get_interceptors(tb_lines)
|
||||
if x["type"] == "ИзменениеИКонтроль" and x["method"].lower() == mname.lower()), None)
|
||||
if not ic:
|
||||
continue
|
||||
folder = os.path.join(run_root, rel_no_bsl, m["canonical"])
|
||||
results.append(resync_one(tb, tb_lines, ic, m, logical_module, folder, report_only))
|
||||
|
||||
total = len(results)
|
||||
actual = sum(1 for r in results if r["status"] == "АКТУАЛЕН")
|
||||
print("[%s] %s -> %s (на контроле: %d)" % (verb, ext_name, cp, total))
|
||||
listed = [r for r in results if r["status"] != "АКТУАЛЕН"]
|
||||
for pr in listed:
|
||||
line = " %-16s %s" % (pr["status"], pr["id"])
|
||||
if pr.get("reason"):
|
||||
line += " %s" % pr["reason"]
|
||||
print(line)
|
||||
if check_mode:
|
||||
drift = sum(1 for r in results if r["status"] == "ДРЕЙФ")
|
||||
confl = sum(1 for r in results if r["status"] == "КОНФЛИКТ")
|
||||
gone = sum(1 for r in results if r["status"] in ("МЕТОД-ИСЧЕЗ", "ИСТОЧНИК-НЕ-НАЙДЕН"))
|
||||
print("Итог: %d/%d актуальны · %d дрейф · %d конфликт · %d требуют внимания"
|
||||
% (actual, total, drift, confl, gone))
|
||||
if listed:
|
||||
print("Починить: /cfe-patch-method -Actualize -ExtensionPath %s -ConfigPath %s" % (extension_path, cp))
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
else:
|
||||
upd = sum(1 for r in results if r["status"] == "АКТУАЛИЗИРОВАН")
|
||||
part = sum(1 for r in results if r["status"] == "ЧАСТИЧНО")
|
||||
print("Итог: %d/%d актуальны · %d актуализировано · %d частично" % (actual, total, upd, part))
|
||||
idx = write_resync_index(run_root, results, ext_name, cp, verb)
|
||||
if idx:
|
||||
print("Merge-воркспейс конфликтов (см. index.md): %s" % idx)
|
||||
sys.exit(0)
|
||||
|
||||
# --- Generation mode: require ModulePath + MethodName + InterceptorType ---
|
||||
if not (module_path and method_name and interceptor_type):
|
||||
die("Нужны -ModulePath, -MethodName, -InterceptorType (генерация перехватчика). "
|
||||
"Для проверки/актуализации контролируемых методов используйте -Check или -Actualize.")
|
||||
|
||||
# --- Resolve module file paths (ModulePath = logical name OR path to a .bsl) ---
|
||||
has_config = bool(config_path)
|
||||
@@ -580,7 +688,31 @@ def main():
|
||||
% (decorator_ru, method_name))
|
||||
print(" Файл: %s" % ext_bsl)
|
||||
sys.exit(0)
|
||||
resync(ext_bsl, ext_lines, dup, method, method_name, module_path)
|
||||
rel = rel_parts_under(extension_path, ext_bsl)
|
||||
logical_module = module_path_from_rel(rel)
|
||||
rel_no_bsl = os.path.join(*rel)
|
||||
if rel_no_bsl.endswith(".bsl"):
|
||||
rel_no_bsl = rel_no_bsl[:-4]
|
||||
safe = re.sub(r'[\\/:*?"<>|]', "_", ext_name)
|
||||
run_root = os.path.join(tempfile.gettempdir(), "cfe-resync", safe)
|
||||
folder = os.path.join(run_root, rel_no_bsl, method["canonical"])
|
||||
res = resync_one(ext_bsl, ext_lines, dup, method, logical_module, folder, False)
|
||||
st = res["status"]
|
||||
if st == "АКТУАЛЕН":
|
||||
print('[АКТУАЛЕН] &ИзменениеИКонтроль("%s") — оригинал не менялся, изменений нет.' % method_name)
|
||||
elif st == "АКТУАЛИЗИРОВАН":
|
||||
print('[АКТУАЛИЗИРОВАН] &ИзменениеИКонтроль("%s") — тело обновлено, перенесено правок: %d'
|
||||
% (method_name, res.get("transferred", 0)))
|
||||
elif st == "ЧАСТИЧНО":
|
||||
idx = write_resync_index(run_root, [res], ext_name, config_path, "АКТУАЛИЗАЦИЯ")
|
||||
print('[АКТУАЛИЗИРОВАН-ЧАСТИЧНО] &ИзменениеИКонтроль("%s") — перенесено: %d, конфликтов: %d'
|
||||
% (method_name, res.get("transferred", 0), res.get("disputed", 0)))
|
||||
print(" Конфликт помечен // [РЕСИНК-КОНФЛИКТ]. Папка метода: %s" % res.get("conflict_dir"))
|
||||
if idx:
|
||||
print(" Индекс: %s" % idx)
|
||||
else:
|
||||
print('[%s] &ИзменениеИКонтроль("%s") — %s' % (st, method_name, res.get("reason", "")))
|
||||
print(" Файл: %s" % ext_bsl)
|
||||
sys.exit(0)
|
||||
|
||||
# --- New interceptor ---
|
||||
@@ -688,16 +820,87 @@ def place_new(ext_bsl, ext_lines, ext_exists, chain, core):
|
||||
place_new.placement = "создан модуль"
|
||||
|
||||
|
||||
def resync(ext_bsl, ext_lines, dup, method, method_name, module_path):
|
||||
DIR_TO_TYPE = {
|
||||
"Catalogs": "Catalog", "Documents": "Document", "Enums": "Enum", "CommonModules": "CommonModule",
|
||||
"Reports": "Report", "DataProcessors": "DataProcessor", "ExchangePlans": "ExchangePlan",
|
||||
"ChartsOfAccounts": "ChartOfAccounts", "ChartsOfCharacteristicTypes": "ChartOfCharacteristicTypes",
|
||||
"ChartsOfCalculationTypes": "ChartOfCalculationTypes", "BusinessProcesses": "BusinessProcess",
|
||||
"Tasks": "Task", "InformationRegisters": "InformationRegister",
|
||||
"AccumulationRegisters": "AccumulationRegister", "AccountingRegisters": "AccountingRegister",
|
||||
"CalculationRegisters": "CalculationRegister",
|
||||
}
|
||||
|
||||
|
||||
def module_path_from_rel(rel_parts):
|
||||
dir0, name = rel_parts[0], rel_parts[1]
|
||||
typ = DIR_TO_TYPE.get(dir0, dir0)
|
||||
if dir0 == "CommonModules":
|
||||
return "CommonModule.%s" % name
|
||||
if len(rel_parts) >= 7 and rel_parts[2] == "Forms":
|
||||
return "%s.%s.Form.%s" % (typ, name, rel_parts[3])
|
||||
mod = rel_parts[-1][:-4] if rel_parts[-1].endswith(".bsl") else rel_parts[-1]
|
||||
return "%s.%s.%s" % (typ, name, mod)
|
||||
|
||||
|
||||
def rel_parts_under(root, full_path):
|
||||
rel = os.path.relpath(os.path.abspath(full_path), os.path.abspath(root))
|
||||
return [s for s in rel.replace("\\", "/").split("/") if s and s != "."]
|
||||
|
||||
|
||||
def conflict_reason(disputed):
|
||||
kinds = set(d["kind"] for d in disputed)
|
||||
parts = []
|
||||
if "insert" in kinds:
|
||||
parts.append("якорь вставки изменён")
|
||||
if "delete" in kinds:
|
||||
parts.append("удаляемое исчезло")
|
||||
return "; ".join(parts)
|
||||
|
||||
|
||||
def write_conflict_folder(folder, method_id, ext_bsl, existing_name, method, v1, marked_body, v2, v1norm, v2norm, disputed):
|
||||
os.makedirs(folder, exist_ok=True)
|
||||
write_bsl(os.path.join(folder, "base.bsl"), v1)
|
||||
write_bsl(os.path.join(folder, "local.bsl"), marked_body)
|
||||
write_bsl(os.path.join(folder, "remote.bsl"), v2)
|
||||
md = []
|
||||
md.append("# %s" % method_id)
|
||||
md.append("Править: %s" % ext_bsl)
|
||||
md.append('Метод: %s (&ИзменениеИКонтроль("%s"))' % (existing_name, method["canonical"]))
|
||||
md.append("Причина: %s" % conflict_reason(disputed))
|
||||
md.append("")
|
||||
md.append("## Не размещено — перенести в метод вручную")
|
||||
for d in disputed:
|
||||
if d["kind"] == "insert":
|
||||
md.append("#Вставка"); md.extend(d["lines"]); md.append("#КонецВставки")
|
||||
else:
|
||||
md.append("// не удалось найти для удаления:")
|
||||
for l in d["lines"]:
|
||||
md.append("// " + l.strip())
|
||||
md.append("")
|
||||
md.append("## Дифф base→remote (что изменилось в оригинале)")
|
||||
for l in v1:
|
||||
if normalize(l) not in v2norm:
|
||||
md.append("- " + l)
|
||||
for l in v2:
|
||||
if normalize(l) not in v1norm:
|
||||
md.append("+ " + l)
|
||||
md.append("")
|
||||
md.append("Рядом: base.bsl / local.bsl / remote.bsl")
|
||||
write_bsl(os.path.join(folder, "conflict.md"), md)
|
||||
|
||||
|
||||
def resync_one(ext_bsl, ext_lines, dup, method, logical_module, conflict_folder, report_only):
|
||||
method_id = "%s.%s" % (logical_module, method["canonical"])
|
||||
dec_line = dup["line"]
|
||||
sig_line_idx = dec_line + 1
|
||||
name_re = re.compile(r'^\s*(?:Асинх\s+)?(?:Процедура|Функция)\s+([\w]+)\s*\(', re.IGNORECASE)
|
||||
if sig_line_idx >= len(ext_lines) or not name_re.match(ext_lines[sig_line_idx]):
|
||||
die('Не удалось найти сигнатуру существующего перехватчика &ИзменениеИКонтроль("%s")' % method_name)
|
||||
existing_name = name_re.match(ext_lines[sig_line_idx]).group(1)
|
||||
m0 = name_re.match(ext_lines[sig_line_idx]) if sig_line_idx < len(ext_lines) else None
|
||||
if not m0:
|
||||
return {"id": method_id, "status": "ОШИБКА", "ext_bsl": ext_bsl, "reason": "не разобрать сигнатуру перехватчика"}
|
||||
existing_name = m0.group(1)
|
||||
sig = read_signature(ext_lines, sig_line_idx)
|
||||
if not sig:
|
||||
die("Не удалось разобрать сигнатуру существующего перехватчика")
|
||||
return {"id": method_id, "status": "ОШИБКА", "ext_bsl": ext_bsl, "reason": "не разобрать сигнатуру"}
|
||||
_params, sig_end = sig
|
||||
is_func = bool(re.match(r'^\s*(?:Асинх\s+)?Функция\b', ext_lines[sig_line_idx], re.IGNORECASE))
|
||||
end_re = re.compile(r'^\s*КонецФункции\b' if is_func else r'^\s*КонецПроцедуры\b', re.IGNORECASE)
|
||||
@@ -707,29 +910,18 @@ def resync(ext_bsl, ext_lines, dup, method, method_name, module_path):
|
||||
block_end = j
|
||||
break
|
||||
if block_end < 0:
|
||||
die("Не найден конец существующего перехватчика")
|
||||
return {"id": method_id, "status": "ОШИБКА", "ext_bsl": ext_bsl, "reason": "не найден конец перехватчика"}
|
||||
|
||||
marked_body = ext_lines[sig_end + 1:block_end]
|
||||
parsed = parse_marked_body(marked_body)
|
||||
v1 = parsed["v1"]
|
||||
ops = parsed["ops"]
|
||||
v2 = method["body_lines"]
|
||||
|
||||
v1 = parsed["v1"]; ops = parsed["ops"]; v2 = method["body_lines"]
|
||||
v1norm = [normalize(x) for x in v1]
|
||||
v2norm = [normalize(x) for x in v2]
|
||||
|
||||
if "\n".join(v1norm) == "\n".join(v2norm):
|
||||
print('[АКТУАЛЕН] &ИзменениеИКонтроль("%s") — оригинал не менялся, изменений нет.' % method_name)
|
||||
print(" Файл: %s" % ext_bsl)
|
||||
return
|
||||
|
||||
insert_top = []
|
||||
insert_after = {}
|
||||
del_start = set()
|
||||
del_end = set()
|
||||
disputed = []
|
||||
transferred = [] # for the summary: {"kind", "anchor"}
|
||||
return {"id": method_id, "status": "АКТУАЛЕН", "ext_bsl": ext_bsl}
|
||||
|
||||
insert_top = []; insert_after = {}; del_start = set(); del_end = set(); disputed = []; transferred = 0
|
||||
for op in ops:
|
||||
if op["kind"] == "insert":
|
||||
after = op["after"]
|
||||
@@ -739,26 +931,25 @@ def resync(ext_bsl, ext_lines, dup, method, method_name, module_path):
|
||||
if k is None:
|
||||
disputed.append({"kind": "insert", "lines": op["lines"]})
|
||||
elif k < 0:
|
||||
insert_top.append(op["lines"])
|
||||
transferred.append({"kind": "insert", "anchor": "(в начало метода)"})
|
||||
insert_top.append(op["lines"]); transferred += 1
|
||||
else:
|
||||
insert_after.setdefault(k, []).append(op["lines"])
|
||||
transferred.append({"kind": "insert", "anchor": v2[k]})
|
||||
insert_after.setdefault(k, []).append(op["lines"]); transferred += 1
|
||||
else:
|
||||
keys = v1norm[op["start"]:op["end"] + 1]
|
||||
p = find_unique_run(v2norm, keys)
|
||||
if p >= 0:
|
||||
del_start.add(p)
|
||||
del_end.add(p + len(keys) - 1)
|
||||
transferred.append({"kind": "delete", "anchor": op["lines"][0]})
|
||||
del_start.add(p); del_end.add(p + len(keys) - 1); transferred += 1
|
||||
else:
|
||||
disputed.append({"kind": "delete", "lines": op["lines"]})
|
||||
|
||||
if report_only:
|
||||
st = "КОНФЛИКТ" if disputed else "ДРЕЙФ"
|
||||
return {"id": method_id, "status": st, "ext_bsl": ext_bsl, "transferred": transferred,
|
||||
"disputed": len(disputed), "reason": conflict_reason(disputed)}
|
||||
|
||||
new_body = []
|
||||
for blk in insert_top:
|
||||
new_body.append("#Вставка")
|
||||
new_body.extend(blk)
|
||||
new_body.append("#КонецВставки")
|
||||
new_body.append("#Вставка"); new_body.extend(blk); new_body.append("#КонецВставки")
|
||||
for k in range(len(v2)):
|
||||
if k in del_start:
|
||||
new_body.append("#Удаление")
|
||||
@@ -767,18 +958,13 @@ def resync(ext_bsl, ext_lines, dup, method, method_name, module_path):
|
||||
new_body.append("#КонецУдаления")
|
||||
if k in insert_after:
|
||||
for blk in insert_after[k]:
|
||||
new_body.append("#Вставка")
|
||||
new_body.extend(blk)
|
||||
new_body.append("#КонецВставки")
|
||||
|
||||
new_body.append("#Вставка"); new_body.extend(blk); new_body.append("#КонецВставки")
|
||||
if disputed:
|
||||
new_body.append("\t// [РЕСИНК-КОНФЛИКТ] перенесите блоки ниже вручную — исходный якорь изменился в новой версии оригинала.")
|
||||
new_body.append("\t// Материалы для анализа см. в выводе команды (файлы base/local/remote/merged).")
|
||||
new_body.append("\t// Материалы: см. index.md / conflict.md в merge-воркспейсе (путь в выводе).")
|
||||
for d in disputed:
|
||||
if d["kind"] == "insert":
|
||||
new_body.append("#Вставка")
|
||||
new_body.extend(d["lines"])
|
||||
new_body.append("#КонецВставки")
|
||||
new_body.append("#Вставка"); new_body.extend(d["lines"]); new_body.append("#КонецВставки")
|
||||
else:
|
||||
new_body.append("\t// [РЕСИНК-КОНФЛИКТ] не удалось найти для удаления:")
|
||||
for l in d["lines"]:
|
||||
@@ -798,48 +984,36 @@ def resync(ext_bsl, ext_lines, dup, method, method_name, module_path):
|
||||
block_start = dec_line
|
||||
if dec_line >= 1 and is_context_directive(ext_lines[dec_line - 1].strip()):
|
||||
block_start = dec_line - 1
|
||||
|
||||
out = list(ext_lines[:block_start])
|
||||
out.extend(new_block)
|
||||
out.extend(ext_lines[block_end + 1:])
|
||||
out = list(ext_lines[:block_start]) + new_block + list(ext_lines[block_end + 1:])
|
||||
write_bsl(ext_bsl, out)
|
||||
|
||||
def print_transfer_summary(items):
|
||||
for t in items:
|
||||
if t["kind"] == "insert":
|
||||
print(" • вставка после «%s»" % truncate(t["anchor"]))
|
||||
else:
|
||||
print(" • удаление «%s»" % truncate(t["anchor"]))
|
||||
|
||||
conflict_dir = None
|
||||
if disputed:
|
||||
# merge workspace (git-mergetool convention: base/local/remote/merged)
|
||||
safe = re.sub(r'[\\/:*?"<>|]', '_', module_path)
|
||||
tmp_root = os.path.join(tempfile.gettempdir(), "cfe-resync", safe + "." + method_name)
|
||||
os.makedirs(tmp_root, exist_ok=True)
|
||||
write_bsl(os.path.join(tmp_root, "base.bsl"), v1)
|
||||
write_bsl(os.path.join(tmp_root, "local.bsl"), marked_body)
|
||||
write_bsl(os.path.join(tmp_root, "remote.bsl"), v2)
|
||||
write_bsl(os.path.join(tmp_root, "merged.bsl"), new_block)
|
||||
diff = ["--- base -> remote (что изменилось в оригинале) ---"]
|
||||
for l in v1:
|
||||
if normalize(l) not in v2norm:
|
||||
diff.append("- " + l)
|
||||
for l in v2:
|
||||
if normalize(l) not in v1norm:
|
||||
diff.append("+ " + l)
|
||||
write_bsl(os.path.join(tmp_root, "diff.txt"), diff)
|
||||
conflict_dir = conflict_folder
|
||||
write_conflict_folder(conflict_folder, method_id, ext_bsl, existing_name, method, v1, marked_body, v2, v1norm, v2norm, disputed)
|
||||
status = "ЧАСТИЧНО" if disputed else "АКТУАЛИЗИРОВАН"
|
||||
return {"id": method_id, "status": status, "ext_bsl": ext_bsl, "transferred": transferred,
|
||||
"disputed": len(disputed), "conflict_dir": conflict_dir, "reason": conflict_reason(disputed)}
|
||||
|
||||
print('[АКТУАЛИЗИРОВАН-ЧАСТИЧНО] &ИзменениеИКонтроль("%s") — перенесено: %d, конфликтов: %d'
|
||||
% (method_name, len(transferred), len(disputed)))
|
||||
print_transfer_summary(transferred)
|
||||
print(" Конфликтные блоки помечены // [РЕСИНК-КОНФЛИКТ] — разместите вручную.")
|
||||
print(" Merge-воркспейс (base/local/remote/merged/diff):")
|
||||
print(" %s" % tmp_root)
|
||||
else:
|
||||
print('[АКТУАЛИЗИРОВАН] &ИзменениеИКонтроль("%s") — тело обновлено, перенесено правок: %d'
|
||||
% (method_name, len(transferred)))
|
||||
print_transfer_summary(transferred)
|
||||
print(" Файл: %s" % ext_bsl)
|
||||
|
||||
def write_resync_index(run_root, results, ext_name, config_path, verb):
|
||||
conflicts = [r for r in results if r["status"] == "ЧАСТИЧНО"]
|
||||
if not conflicts:
|
||||
return None
|
||||
os.makedirs(run_root, exist_ok=True)
|
||||
total = len(results)
|
||||
actual = sum(1 for r in results if r["status"] == "АКТУАЛЕН")
|
||||
upd = sum(1 for r in results if r["status"] == "АКТУАЛИЗИРОВАН")
|
||||
lines = []
|
||||
lines.append("[%s] %s -> %s" % (verb, ext_name, config_path))
|
||||
lines.append("Итог: %d/%d актуальны · %d актуализировано · %d конфликтов" % (actual, total, upd, len(conflicts)))
|
||||
lines.append("")
|
||||
lines.append("Конфликты — править .bsl расширения:")
|
||||
for c in conflicts:
|
||||
lines.append(" ЧАСТИЧНО %s" % c["id"])
|
||||
lines.append(" -> %s (%s)" % (c["ext_bsl"], c.get("reason", "")))
|
||||
write_bsl(os.path.join(run_root, "index.md"), lines)
|
||||
return run_root
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user