From 80618f0b93ca121b31bd0ddd6c3285c076d0d589 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Thu, 24 Sep 2026 18:08:20 +0300 Subject: [PATCH] =?UTF-8?q?fix(db-load-git):=20=D0=BF=D1=80=D0=B5=D0=B4?= =?UTF-8?q?=D1=83=D0=BF=D1=80=D0=B5=D0=B6=D0=B4=D0=B0=D1=82=D1=8C=20=D0=BE?= =?UTF-8?q?=D0=B1=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=D1=85?= =?UTF-8?q?,=20=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D1=8B=D0=B5=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B0=20=D1=87=D0=B0=D1=81=D1=82?= =?UTF-8?q?=D1=8F=D0=BC=D0=B8=20=D0=BD=D0=B5=20=D0=BF=D1=80=D0=B8=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Удалённый объект уходит из базы, только если в списке его владелец и в составе владельца объекта уже нет; удалённый файл-часть (модуль, Ext/) загрузка частями не удаляет никогда. Скрипт классифицирует удаления: применимые — [note], остальные — [ВНИМАНИЕ] с причиной; если загружать нечего, а неприменимые удаления есть — код 1. git diff с --no-renames, чтобы старый путь переименования был виден. -AllExtensions отвергается на обоих движках: частями грузится одно расширение за раз. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01ANXwbMUhuRFSCsq2RgmTTm --- .claude/skills/db-load-git/SKILL.md | 3 +- .../db-load-git/scripts/db-load-git.ps1 | 116 ++++++++++++++-- .../skills/db-load-git/scripts/db-load-git.py | 128 ++++++++++++++++-- .../deleted-form-still-in-owner-warns.json | 27 ++++ .../deleted-form-with-owner-applies.json | 25 ++++ .../db-load-git/deleted-module-warns.json | 22 +++ .../deleted-non-config-ignored.json | 20 +++ .../deleted-object-same-name-other-type.json | 20 +++ .../db-load-git/error-all-extensions.json | 11 ++ .../error-only-unapplied-deletion.json | 25 ++++ .../renamed-object-old-path-warns.json | 19 +++ 11 files changed, 385 insertions(+), 31 deletions(-) create mode 100644 tests/skills/cases/db-load-git/deleted-form-still-in-owner-warns.json create mode 100644 tests/skills/cases/db-load-git/deleted-form-with-owner-applies.json create mode 100644 tests/skills/cases/db-load-git/deleted-module-warns.json create mode 100644 tests/skills/cases/db-load-git/deleted-non-config-ignored.json create mode 100644 tests/skills/cases/db-load-git/deleted-object-same-name-other-type.json create mode 100644 tests/skills/cases/db-load-git/error-all-extensions.json create mode 100644 tests/skills/cases/db-load-git/error-only-unapplied-deletion.json create mode 100644 tests/skills/cases/db-load-git/renamed-object-old-path-warns.json diff --git a/.claude/skills/db-load-git/SKILL.md b/.claude/skills/db-load-git/SKILL.md index 8face5679..521f331ef 100644 --- a/.claude/skills/db-load-git/SKILL.md +++ b/.claude/skills/db-load-git/SKILL.md @@ -15,6 +15,8 @@ allowed-tools: > Если в изменениях есть `Configuration.xml`, платформа выполнит **полную загрузку конфигурации**, а не только изменённых объектов. +> Удалённый объект уйдёт из базы, только если удалён и из состава владельца (`ChildObjects` объекта или `Configuration.xml`). Удалённый файл-часть объекта (модуль, файлы `Ext/`) загрузкой частями не удаляется: скрипт перечислит такие удаления в `[ВНИМАНИЕ]`, а если кроме них загружать нечего, завершится ошибкой. Применить их можно только полной загрузкой (`/db-load-xml -Mode Full`); чтобы убрать код модуля, достаточно оставить файл пустым. + ## Usage ``` @@ -58,7 +60,6 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" < | `-CommitRange ` | для Commit | Диапазон коммитов (напр. `HEAD~3..HEAD`) | | `-Extension <имя>` | нет | Загрузить в расширение | | `-NoApplyCheck` | нет | Не проверять применимость расширения после загрузки | -| `-AllExtensions` | нет | Загрузить все расширения | | `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | | `-DryRun` | нет | Только показать что будет загружено (без загрузки) | | `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) | diff --git a/.claude/skills/db-load-git/scripts/db-load-git.ps1 b/.claude/skills/db-load-git/scripts/db-load-git.ps1 index b88ad276b..2dc527a61 100644 --- a/.claude/skills/db-load-git/scripts/db-load-git.ps1 +++ b/.claude/skills/db-load-git/scripts/db-load-git.ps1 @@ -1,4 +1,4 @@ -# db-load-git v1.28 — Load Git changes into 1C database +# db-load-git v1.29 — Load Git changes into 1C database # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # NB: *nix-раскладку платформы (/opt/1cv8//1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. <# @@ -40,7 +40,7 @@ Имя расширения для загрузки .PARAMETER AllExtensions - Загрузить все расширения + Не поддерживается: загрузка частями идёт по одному расширению (-Extension) .PARAMETER Format Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical) @@ -425,6 +425,73 @@ function Get-ObjectXmlFromSubFile { return $null } +# Каталоги выгрузки объектов верхнего уровня → элемент в конфигурации. +$script:TypeDirTag = @{ + "Languages"="Language"; "Subsystems"="Subsystem"; "StyleItems"="StyleItem"; "Styles"="Style" + "CommonPictures"="CommonPicture"; "SessionParameters"="SessionParameter"; "Roles"="Role"; "CommonTemplates"="CommonTemplate" + "FilterCriteria"="FilterCriterion"; "CommonModules"="CommonModule"; "Bots"="Bot"; "PaletteColors"="PaletteColor"; "CommonAttributes"="CommonAttribute"; "ExchangePlans"="ExchangePlan" + "XDTOPackages"="XDTOPackage"; "WebServices"="WebService"; "HTTPServices"="HTTPService"; "WSReferences"="WSReference" + "EventSubscriptions"="EventSubscription"; "ScheduledJobs"="ScheduledJob"; "SettingsStorages"="SettingsStorage"; "FunctionalOptions"="FunctionalOption" + "FunctionalOptionsParameters"="FunctionalOptionsParameter"; "DefinedTypes"="DefinedType"; "CommonCommands"="CommonCommand"; "CommandGroups"="CommandGroup" + "Constants"="Constant"; "CommonForms"="CommonForm"; "Catalogs"="Catalog"; "Documents"="Document" + "DocumentNumerators"="DocumentNumerator"; "Sequences"="Sequence"; "DocumentJournals"="DocumentJournal"; "Enums"="Enum" + "Reports"="Report"; "DataProcessors"="DataProcessor"; "InformationRegisters"="InformationRegister"; "AccumulationRegisters"="AccumulationRegister" + "ChartsOfCharacteristicTypes"="ChartOfCharacteristicTypes"; "ChartsOfAccounts"="ChartOfAccounts"; "AccountingRegisters"="AccountingRegister" + "ChartsOfCalculationTypes"="ChartOfCalculationTypes"; "CalculationRegisters"="CalculationRegister" + "BusinessProcesses"="BusinessProcess"; "Tasks"="Task"; "ExternalDataSources"="ExternalDataSource"; "IntegrationServices"="IntegrationService" +} + +function Get-DeletionVerdicts { + # Что загрузка частями сделает с путями, удалёнными в git. Удалённый ОБЪЕКТ (его XML-описание) + # исчезнет из базы, только если в списке его владелец и в составе владельца (ChildObjects) + # объекта уже нет. Удалённую ЧАСТЬ объекта (модуль, файлы Ext/) загрузка частями не удаляет + # никогда — платформа молча пропускает отсутствующий файл, даже перечисленный явно. + # Пути вне каталогов объектов (README, docs/…) — не конфигурация, их удаление не оценивается. + # Возвращает вердикт по каждому удалению: @{ Path; Applied; Reason }. + param([string[]]$Deleted, [string[]]$Loaded, [string]$Root, [string]$Format) + $result = @() + $ours = @($Deleted | Where-Object { $script:TypeDirTag.ContainsKey(($_ -split '/')[0]) -and ($_ -split '/').Count -ge 2 }) + if ($Format -eq "Plain") { + foreach ($d in $ours) { $result += @{ Path = $d; Applied = $false; Reason = "формат Plain — удаление загрузкой частями не применяется" } } + return $result + } + foreach ($d in $ours) { + $segs = $d -split '/' + $extIdx = [array]::IndexOf($segs, 'Ext') + if ($extIdx -lt 0 -and $d -match '\.xml$' -and ($segs.Count -eq 2 -or $segs.Count -ge 4)) { + # Описание объекта: владелец — Configuration.xml или описание объекта двумя уровнями выше. + # Элемент состава — по каталогу: Catalogs → Catalog, Forms → Form. + if ($segs.Count -eq 2) { + $owner = "Configuration.xml" + $tag = $script:TypeDirTag[$segs[0]] + } else { + $owner = ($segs[0..($segs.Count - 3)] -join '/') + ".xml" + $tag = $segs[-2] -replace 's$', '' + } + if ($Deleted -contains $owner) { continue } + $name = [System.IO.Path]::GetFileNameWithoutExtension($segs[-1]) + if ($Loaded -notcontains $owner) { + $result += @{ Path = $d; Applied = $false; Reason = "владелец $owner не изменён — состав в базе прежний" } + continue + } + $ownerText = [System.IO.File]::ReadAllText((Join-Path $Root $owner)) + $m = [regex]::Match($ownerText, '(?s).*') + $pattern = '(?m)^\s*<' + $tag + '>' + [regex]::Escape($name) + '\s*$' + if ($m.Success -and [regex]::IsMatch($m.Value, $pattern)) { + $result += @{ Path = $d; Applied = $false; Reason = "$owner всё ещё содержит объект в составе" } + } else { + $result += @{ Path = $d; Applied = $true; Reason = "через $owner" } + } + continue + } + if ($extIdx -lt 0) { continue } + # Часть объекта: сам объект — сегменты до Ext. Удалён вместе с объектом — судьба объекта. + if ($Deleted -contains (($segs[0..($extIdx - 1)] -join '/') + ".xml")) { continue } + $result += @{ Path = $d; Applied = $false; Reason = "часть объекта — загрузка частями файлы не удаляет (пустой файл вместо удаления очистит модуль)" } + } + return $result +} + # --- Resolve V8Path (skip if DryRun) --- if (-not $DryRun) { function Find-ProjectV8Path { @@ -677,6 +744,14 @@ if ($Source -eq "Commit" -and -not $CommitRange) { exit 1 } +# --- -AllExtensions: загрузка частями идёт по одному расширению --- +# Конфигуратор сочетание -AllExtensions с -listFile отвергает сам, ibcmd его не поддерживает. +if ($AllExtensions) { + Write-Host "Error: -AllExtensions cannot be combined with a partial load" -ForegroundColor Red + Write-Host " Загружайте расширения по одному: -Extension <имя>, -ConfigDir — каталог этого расширения." -ForegroundColor Yellow + exit 1 +} + # --- Check git --- try { $null = git --version 2>&1 @@ -688,6 +763,8 @@ try { # --- Get changed files from Git --- # Все git-вызовы для сбора путей идут через один хелпер с -c core.quotePath=false, # иначе кириллические пути возвращаются в octal-виде и не распознаются (зеркало run_git в .py). +# diff — с --no-renames: при переименовании иначе виден только новый путь, а старый — это +# удаление, о котором надо предупредить. function Invoke-GitLines { param([string[]]$GitArgs) $out = git -c core.quotePath=false @GitArgs 2>&1 @@ -704,21 +781,21 @@ try { switch ($Source) { "Staged" { Write-Host "Getting staged changes..." - $changedFiles += Invoke-GitLines -GitArgs @('diff', '--cached', '--name-only', '--relative') + $changedFiles += Invoke-GitLines -GitArgs @('diff', '--cached', '--name-only', '--no-renames', '--relative') } "Unstaged" { Write-Host "Getting unstaged changes..." - $changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative') + $changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--no-renames', '--relative') $changedFiles += Invoke-GitLines -GitArgs @('ls-files', '--others', '--exclude-standard') } "Commit" { Write-Host "Getting changes from $CommitRange..." - $changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative', $CommitRange) + $changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--no-renames', '--relative', $CommitRange) } "All" { Write-Host "Getting all uncommitted changes..." - $changedFiles += Invoke-GitLines -GitArgs @('diff', '--cached', '--name-only', '--relative') - $changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative') + $changedFiles += Invoke-GitLines -GitArgs @('diff', '--cached', '--name-only', '--no-renames', '--relative') + $changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--no-renames', '--relative') $changedFiles += Invoke-GitLines -GitArgs @('ls-files', '--others', '--exclude-standard') } } @@ -738,6 +815,7 @@ Write-Host "Git changes detected: $($changedFiles.Count) files" # --- Filter and map to config files --- $configFiles = @() $supportSkipped = @() +$deletedFiles = @() foreach ($file in $changedFiles) { $file = $file.Trim().Replace('\', '/') @@ -749,6 +827,7 @@ foreach ($file in $changedFiles) { if ($file -eq "ConfigDumpInfo.xml" -or $file -match '(^|/)ConfigDumpInfo\.xml$') { continue } $fullPath = Join-Path $ConfigDir $file + if (-not (Test-Path -LiteralPath $fullPath)) { $deletedFiles += $file } if ($file -match '\.xml$') { # XML file — add directly if exists @@ -795,7 +874,20 @@ if ($supportSkipped.Count -gt 0) { Write-Host " Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full)." -ForegroundColor Yellow } +$verdicts = @(Get-DeletionVerdicts -Deleted $deletedFiles -Loaded $configFiles -Root $ConfigDir -Format $Format) +$unapplied = @($verdicts | Where-Object { -not $_.Applied }) +foreach ($v in @($verdicts | Where-Object { $_.Applied })) { Write-Host "[note] удаление применится $($v.Reason): $($v.Path)" } +if ($unapplied.Count -gt 0) { + Write-Host "[ВНИМАНИЕ] Удаления, которые загрузка частями НЕ применит — в базе они останутся:" -ForegroundColor Yellow + foreach ($u in $unapplied) { Write-Host " - $($u.Path): $($u.Reason)" -ForegroundColor Yellow } + Write-Host " Применить их можно полной загрузкой (db-load-xml -Mode Full)." -ForegroundColor Yellow +} + if ($configFiles.Count -eq 0) { + if ($unapplied.Count -gt 0) { + Write-Host "Error: changes found, but none of them can be applied by a partial load (see above)" -ForegroundColor Red + exit 1 + } Write-Host "No configuration files found in changes" exit 0 } @@ -829,10 +921,6 @@ try { Write-Host "Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red exit 1 } - if ($AllExtensions) { - Write-Host "Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)" -ForegroundColor Red - exit 1 - } $arguments = @("infobase", "config", "import", "files") + $configFiles $arguments += "--base-dir=$ConfigDir", "--db-path=$InfoBasePath" if ($Extension) { $arguments += "--extension=$Extension" } @@ -867,7 +955,7 @@ try { Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red } # Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме. - if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) { + if ($exitCode -eq 0 -and $Extension -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) { $acConn = @("/F", "`"$InfoBasePath`"") if ($UserName) { $acConn += "/N`"$UserName`"" } if ($Password) { $acConn += "/P`"$Password`"" } @@ -913,8 +1001,6 @@ try { # --- Extensions --- if ($Extension) { $arguments += "-Extension", "`"$Extension`"" - } elseif ($AllExtensions) { - $arguments += "-AllExtensions" } # --- UpdateDB --- @@ -967,7 +1053,7 @@ try { } # Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу. - if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) { + if ($exitCode -eq 0 -and $Extension -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) { if ((Invoke-ApplyCheckReport $V8Path $connArgs $Extension $extraArgs) -and $StrictLog) { $exitCode = 1 } } diff --git a/.claude/skills/db-load-git/scripts/db-load-git.py b/.claude/skills/db-load-git/scripts/db-load-git.py index 5759e7563..337ab1ba4 100644 --- a/.claude/skills/db-load-git/scripts/db-load-git.py +++ b/.claude/skills/db-load-git/scripts/db-load-git.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# db-load-git v1.28 — Load Git changes into 1C database +# db-load-git v1.29 — Load Git changes into 1C database # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -619,8 +619,87 @@ def get_object_xml_from_subfile(relative_path): return None +# Каталоги выгрузки объектов верхнего уровня → элемент в конфигурации. +TYPE_DIR_TAG = { + "Languages": "Language", "Subsystems": "Subsystem", "StyleItems": "StyleItem", "Styles": "Style", + "CommonPictures": "CommonPicture", "SessionParameters": "SessionParameter", "Roles": "Role", + "CommonTemplates": "CommonTemplate", "FilterCriteria": "FilterCriterion", "CommonModules": "CommonModule", + "Bots": "Bot", "PaletteColors": "PaletteColor", "CommonAttributes": "CommonAttribute", + "ExchangePlans": "ExchangePlan", "XDTOPackages": "XDTOPackage", "WebServices": "WebService", + "HTTPServices": "HTTPService", "WSReferences": "WSReference", "EventSubscriptions": "EventSubscription", + "ScheduledJobs": "ScheduledJob", "SettingsStorages": "SettingsStorage", "FunctionalOptions": "FunctionalOption", + "FunctionalOptionsParameters": "FunctionalOptionsParameter", "DefinedTypes": "DefinedType", + "CommonCommands": "CommonCommand", "CommandGroups": "CommandGroup", "Constants": "Constant", + "CommonForms": "CommonForm", "Catalogs": "Catalog", "Documents": "Document", + "DocumentNumerators": "DocumentNumerator", "Sequences": "Sequence", "DocumentJournals": "DocumentJournal", + "Enums": "Enum", "Reports": "Report", "DataProcessors": "DataProcessor", + "InformationRegisters": "InformationRegister", "AccumulationRegisters": "AccumulationRegister", + "ChartsOfCharacteristicTypes": "ChartOfCharacteristicTypes", "ChartsOfAccounts": "ChartOfAccounts", + "AccountingRegisters": "AccountingRegister", "ChartsOfCalculationTypes": "ChartOfCalculationTypes", + "CalculationRegisters": "CalculationRegister", "BusinessProcesses": "BusinessProcess", "Tasks": "Task", + "ExternalDataSources": "ExternalDataSource", "IntegrationServices": "IntegrationService", +} +_TYPE_DIR_TAG_CI = {k.lower(): v for k, v in TYPE_DIR_TAG.items()} + + +def get_deletion_verdicts(deleted, loaded, root, fmt): + """Что загрузка частями сделает с путями, удалёнными в git. Удалённый ОБЪЕКТ (его XML-описание) + исчезнет из базы, только если в списке его владелец и в составе владельца (ChildObjects) + объекта уже нет. Удалённую ЧАСТЬ объекта (модуль, файлы Ext/) загрузка частями не удаляет + никогда — платформа молча пропускает отсутствующий файл, даже перечисленный явно. + Пути вне каталогов объектов (README, docs/…) — не конфигурация, их удаление не оценивается. + Возвращает вердикт по каждому удалению: {path, applied, reason}.""" + ours = [d for d in deleted if len(d.split("/")) >= 2 and d.split("/")[0].lower() in _TYPE_DIR_TAG_CI] + if fmt == "Plain": + return [{"path": d, "applied": False, + "reason": "формат Plain — удаление загрузкой частями не применяется"} for d in ours] + deleted_set = {d.lower() for d in deleted} + loaded_set = {x.lower() for x in loaded} + result = [] + for d in ours: + segs = d.split("/") + ext_idx = segs.index("Ext") if "Ext" in segs else -1 + if ext_idx < 0 and d.lower().endswith(".xml") and (len(segs) == 2 or len(segs) >= 4): + # Описание объекта: владелец — Configuration.xml или описание объекта двумя уровнями выше. + # Элемент состава — по каталогу: Catalogs → Catalog, Forms → Form. + if len(segs) == 2: + owner = "Configuration.xml" + tag = _TYPE_DIR_TAG_CI[segs[0].lower()] + else: + owner = "/".join(segs[:-2]) + ".xml" + tag = re.sub(r"s$", "", segs[-2]) + if owner.lower() in deleted_set: + continue + name = os.path.splitext(segs[-1])[0] + if owner.lower() not in loaded_set: + result.append({"path": d, "applied": False, + "reason": f"владелец {owner} не изменён — состав в базе прежний"}) + continue + with open(os.path.join(root, owner), encoding="utf-8-sig") as f: + owner_text = f.read() + m = re.search(r".*", owner_text, re.S) + pattern = r"(?m)^\s*<" + tag + ">" + re.escape(name) + "\s*$" + if m and re.search(pattern, m.group(0)): + result.append({"path": d, "applied": False, + "reason": f"{owner} всё ещё содержит объект в составе"}) + else: + result.append({"path": d, "applied": True, "reason": f"через {owner}"}) + continue + if ext_idx < 0: + continue + # Часть объекта: сам объект — сегменты до Ext. Удалён вместе с объектом — судьба объекта. + if ("/".join(segs[:ext_idx]) + ".xml").lower() in deleted_set: + continue + result.append({"path": d, "applied": False, + "reason": "часть объекта — загрузка частями файлы не удаляет " + "(пустой файл вместо удаления очистит модуль)"}) + return result + + def run_git(config_dir, git_args): - """Run a git command in config_dir and return output lines on success.""" + """Run a git command in config_dir and return output lines on success. + diff вызывается с --no-renames: при переименовании иначе виден только новый путь, а старый — + это удаление, о котором надо предупредить.""" result = subprocess.run( ["git", "-c", "core.quotePath=false"] + git_args, capture_output=True, @@ -691,7 +770,7 @@ def main(): ) parser.add_argument("-CommitRange", default="", help="Commit range (for Source=Commit), e.g. HEAD~3..HEAD") parser.add_argument("-Extension", default="", help="Extension name to load") - parser.add_argument("-AllExtensions", action="store_true", help="Load all extensions") + parser.add_argument("-AllExtensions", action="store_true", help="Not supported: partial load goes one extension at a time") parser.add_argument( "-Format", default="Hierarchical", @@ -758,6 +837,13 @@ def main(): print("Error: -CommitRange required for Source=Commit") sys.exit(1) + # --- -AllExtensions: загрузка частями идёт по одному расширению --- + # Конфигуратор сочетание -AllExtensions с -listFile отвергает сам, ibcmd его не поддерживает. + if args.AllExtensions: + print("Error: -AllExtensions cannot be combined with a partial load") + print(" Загружайте расширения по одному: -Extension <имя>, -ConfigDir — каталог этого расширения.") + sys.exit(1) + # --- Check git --- try: subprocess.run(["git", "--version"], capture_output=True, text=True, check=True) @@ -770,18 +856,18 @@ def main(): if args.Source == "Staged": print("Getting staged changes...") - changed_files += run_git(args.ConfigDir, ["diff", "--cached", "--name-only", "--relative"]) + changed_files += run_git(args.ConfigDir, ["diff", "--cached", "--name-only", "--no-renames", "--relative"]) elif args.Source == "Unstaged": print("Getting unstaged changes...") - changed_files += run_git(args.ConfigDir, ["diff", "--name-only", "--relative"]) + changed_files += run_git(args.ConfigDir, ["diff", "--name-only", "--no-renames", "--relative"]) changed_files += run_git(args.ConfigDir, ["ls-files", "--others", "--exclude-standard"]) elif args.Source == "Commit": print(f"Getting changes from {args.CommitRange}...") - changed_files += run_git(args.ConfigDir, ["diff", "--name-only", "--relative", args.CommitRange]) + changed_files += run_git(args.ConfigDir, ["diff", "--name-only", "--no-renames", "--relative", args.CommitRange]) elif args.Source == "All": print("Getting all uncommitted changes...") - changed_files += run_git(args.ConfigDir, ["diff", "--cached", "--name-only", "--relative"]) - changed_files += run_git(args.ConfigDir, ["diff", "--name-only", "--relative"]) + changed_files += run_git(args.ConfigDir, ["diff", "--cached", "--name-only", "--no-renames", "--relative"]) + changed_files += run_git(args.ConfigDir, ["diff", "--name-only", "--no-renames", "--relative"]) changed_files += run_git(args.ConfigDir, ["ls-files", "--others", "--exclude-standard"]) # Deduplicate and filter blanks @@ -796,6 +882,7 @@ def main(): # --- Filter and map to config files --- config_files = [] support_skipped = [] + deleted_files = [] for file in changed_files: file = file.strip().replace("\\", "/") @@ -811,6 +898,8 @@ def main(): continue full_path = os.path.join(args.ConfigDir, file) + if not os.path.exists(full_path): + deleted_files.append(file) if file.endswith(".xml"): # XML file — add directly if exists @@ -846,7 +935,21 @@ def main(): print(f" - {sf}") print(" Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full).") + verdicts = get_deletion_verdicts(deleted_files, config_files, args.ConfigDir, args.Format) + unapplied = [v for v in verdicts if not v["applied"]] + for v in verdicts: + if v["applied"]: + print(f"[note] удаление применится {v['reason']}: {v['path']}") + if unapplied: + print("[ВНИМАНИЕ] Удаления, которые загрузка частями НЕ применит — в базе они останутся:") + for u in unapplied: + print(f" - {u['path']}: {u['reason']}") + print(" Применить их можно полной загрузкой (db-load-xml -Mode Full).") + if len(config_files) == 0: + if unapplied: + print("Error: changes found, but none of them can be applied by a partial load (see above)") + sys.exit(1) print("No configuration files found in changes") sys.exit(0) @@ -877,9 +980,6 @@ def main(): if args.Format == "Plain": print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)") sys.exit(1) - if args.AllExtensions: - print("Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)") - sys.exit(1) arguments = ["infobase", "config", "import", "files"] + config_files arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"] if args.Extension: @@ -916,7 +1016,7 @@ def main(): print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}") print_platform_output(ar) # Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме. - if (exit_code == 0 and (args.Extension or args.AllExtensions) + if (exit_code == 0 and args.Extension and apply_check_enabled(args.NoApplyCheck)): ac_conn = ["/F", f'"{args.InfoBasePath}"'] if args.UserName: @@ -963,8 +1063,6 @@ def main(): # --- Extensions --- if args.Extension: arguments += ["-Extension", f'"{args.Extension}"'] - elif args.AllExtensions: - arguments.append("-AllExtensions") # --- UpdateDB --- if args.UpdateDB: @@ -1021,7 +1119,7 @@ def main(): exit_code = 1 # Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу. - if (exit_code == 0 and (args.Extension or args.AllExtensions) + if (exit_code == 0 and args.Extension and apply_check_enabled(args.NoApplyCheck)): if apply_check_report(v8path, conn_args, args.Extension, extra_args) and args.StrictLog: exit_code = 1 diff --git a/tests/skills/cases/db-load-git/deleted-form-still-in-owner-warns.json b/tests/skills/cases/db-load-git/deleted-form-still-in-owner-warns.json new file mode 100644 index 000000000..627126185 --- /dev/null +++ b/tests/skills/cases/db-load-git/deleted-form-still-in-owner-warns.json @@ -0,0 +1,27 @@ +{ + "name": "Файлы формы удалены, а владелец всё ещё держит её в составе: удаление не применится", + "preRun": [ + { "writeFile": { "path": "src/Configuration.xml", "content": "\n\t\n\t\t\n\t\t\tТовары\n\t\t\n\t\n\n" } }, + { "writeFile": { "path": "src/Catalogs/Товары.xml", "content": "\n\t\n\t\t\n\t\t\t
ФормаЭлемента
\n\t\t
\n\t
\n
\n" } }, + { "writeFile": { "path": "src/Catalogs/Товары/Forms/ФормаЭлемента.xml", "content": "\n" } }, + { "writeFile": { "path": "src/Catalogs/Товары/Forms/ФормаЭлемента/Ext/Form.xml", "content": "
\n" } }, + { "git": ["-C", "src", "init", "-q"] }, + { "git": ["-C", "src", "add", "-A"] }, + { "git": ["-C", "src", "commit", "-qm", "base"] }, + { "writeFile": { "path": "src/Catalogs/Товары.xml", "content": "\n\t\n\t\tправка\n\t\t\n\t\t\tФормаЭлемента\n\t\t\n\t\n
\n" } }, + { "deletePath": "src/Catalogs/Товары/Forms" }, + { "git": ["-C", "src", "add", "-A"] } + ], + "args_extra": ["-ConfigDir", "{workDir}/src", "-Source", "Staged", "-DryRun"], + "expect": { + "stdoutContains": [ + "Catalogs/Товары/Forms/ФормаЭлемента.xml: Catalogs/Товары.xml всё ещё содержит объект в составе", + " Catalogs/Товары.xml" + ], + "stdoutNotContains": [ + "[note] удаление применится", + "Ext/Form.xml:" + ] + }, + "noSnapshot": "сухой прогон ничего не пишет — проверяется вердикт по удалению" +} diff --git a/tests/skills/cases/db-load-git/deleted-form-with-owner-applies.json b/tests/skills/cases/db-load-git/deleted-form-with-owner-applies.json new file mode 100644 index 000000000..628dbd5b2 --- /dev/null +++ b/tests/skills/cases/db-load-git/deleted-form-with-owner-applies.json @@ -0,0 +1,25 @@ +{ + "name": "Форма удалена вместе с правкой владельца: удаление применится через владельца", + "preRun": [ + { "writeFile": { "path": "src/Configuration.xml", "content": "\n\t\n\t\t\n\t\t\tТовары\n\t\t\n\t\n\n" } }, + { "writeFile": { "path": "src/Catalogs/Товары.xml", "content": "\n\t\n\t\t\n\t\t\t
ФормаЭлемента
\n\t\t
\n\t
\n
\n" } }, + { "writeFile": { "path": "src/Catalogs/Товары/Forms/ФормаЭлемента.xml", "content": "\n" } }, + { "writeFile": { "path": "src/Catalogs/Товары/Forms/ФормаЭлемента/Ext/Form.xml", "content": "
\n" } }, + { "writeFile": { "path": "src/Catalogs/Товары/Forms/ФормаЭлемента/Ext/Form/Module.bsl", "content": "\n" } }, + { "git": ["-C", "src", "init", "-q"] }, + { "git": ["-C", "src", "add", "-A"] }, + { "git": ["-C", "src", "commit", "-qm", "base"] }, + { "writeFile": { "path": "src/Catalogs/Товары.xml", "content": "\n\t\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\tФормаЭлемента\n\t\t\t\t\n\t\t\t\n\t\t\n\t\n\n" } }, + { "deletePath": "src/Catalogs/Товары/Forms" }, + { "git": ["-C", "src", "add", "-A"] } + ], + "args_extra": ["-ConfigDir", "{workDir}/src", "-Source", "Staged", "-DryRun"], + "expect": { + "stdoutContains": [ + "[note] удаление применится через Catalogs/Товары.xml: Catalogs/Товары/Forms/ФормаЭлемента.xml", + " Catalogs/Товары.xml" + ], + "stdoutNotContains": "[ВНИМАНИЕ] Удаления" + }, + "noSnapshot": "сухой прогон ничего не пишет — проверяется вердикт по удалению" +} diff --git a/tests/skills/cases/db-load-git/deleted-module-warns.json b/tests/skills/cases/db-load-git/deleted-module-warns.json new file mode 100644 index 000000000..1b4cbf0cd --- /dev/null +++ b/tests/skills/cases/db-load-git/deleted-module-warns.json @@ -0,0 +1,22 @@ +{ + "name": "Удалённый модуль объекта: загрузка частями его не удалит — [ВНИМАНИЕ]", + "preRun": [ + { "writeFile": { "path": "src/Configuration.xml", "content": "\n\t\n\t\t\n\t\t\tТовары\n\t\t\n\t\n\n" } }, + { "writeFile": { "path": "src/Catalogs/Товары.xml", "content": "\n\t\n\t\t\n\t\n\n" } }, + { "writeFile": { "path": "src/Catalogs/Товары/Ext/ObjectModule.bsl", "content": "Процедура А() КонецПроцедуры\n" } }, + { "git": ["-C", "src", "init", "-q"] }, + { "git": ["-C", "src", "add", "-A"] }, + { "git": ["-C", "src", "commit", "-qm", "base"] }, + { "deletePath": "src/Catalogs/Товары/Ext/ObjectModule.bsl" }, + { "git": ["-C", "src", "add", "-A"] } + ], + "args_extra": ["-ConfigDir", "{workDir}/src", "-Source", "Staged", "-DryRun"], + "expect": { + "stdoutContains": [ + "[ВНИМАНИЕ] Удаления, которые загрузка частями НЕ применит — в базе они останутся:", + "Catalogs/Товары/Ext/ObjectModule.bsl: часть объекта — загрузка частями файлы не удаляет", + "DryRun mode - no changes applied" + ] + }, + "noSnapshot": "сухой прогон ничего не пишет — проверяется вердикт по удалению" +} diff --git a/tests/skills/cases/db-load-git/deleted-non-config-ignored.json b/tests/skills/cases/db-load-git/deleted-non-config-ignored.json new file mode 100644 index 000000000..eae520a55 --- /dev/null +++ b/tests/skills/cases/db-load-git/deleted-non-config-ignored.json @@ -0,0 +1,20 @@ +{ + "name": "Удалены файлы вне конфигурации (README, docs): не удаления объектов, выход без ошибки", + "preRun": [ + { "writeFile": { "path": "src/Configuration.xml", "content": "\n\t\n\t\t\n\t\n\n" } }, + { "writeFile": { "path": "src/README.md", "content": "readme\n" } }, + { "writeFile": { "path": "src/docs/notes.xml", "content": "\n" } }, + { "git": ["-C", "src", "init", "-q"] }, + { "git": ["-C", "src", "add", "-A"] }, + { "git": ["-C", "src", "commit", "-qm", "base"] }, + { "deletePath": "src/README.md" }, + { "deletePath": "src/docs" }, + { "git": ["-C", "src", "add", "-A"] } + ], + "args_extra": ["-ConfigDir", "{workDir}/src", "-Source", "Staged", "-DryRun"], + "expect": { + "stdoutContains": "No configuration files found in changes", + "stdoutNotContains": "[ВНИМАНИЕ] Удаления" + }, + "noSnapshot": "сухой прогон ничего не пишет — проверяется, что посторонние удаления не оцениваются" +} diff --git a/tests/skills/cases/db-load-git/deleted-object-same-name-other-type.json b/tests/skills/cases/db-load-git/deleted-object-same-name-other-type.json new file mode 100644 index 000000000..a1d4652e9 --- /dev/null +++ b/tests/skills/cases/db-load-git/deleted-object-same-name-other-type.json @@ -0,0 +1,20 @@ +{ + "name": "Удалён документ, а справочник с тем же именем остался: удаление применится (состав сверяется по типу)", + "preRun": [ + { "writeFile": { "path": "src/Configuration.xml", "content": "\n\t\n\t\t\n\t\t\tТовары\n\t\t\tТовары\n\t\t\n\t\n\n" } }, + { "writeFile": { "path": "src/Catalogs/Товары.xml", "content": "\n" } }, + { "writeFile": { "path": "src/Documents/Товары.xml", "content": "\n" } }, + { "git": ["-C", "src", "init", "-q"] }, + { "git": ["-C", "src", "add", "-A"] }, + { "git": ["-C", "src", "commit", "-qm", "base"] }, + { "writeFile": { "path": "src/Configuration.xml", "content": "\n\t\n\t\t\n\t\t\tТовары\n\t\t\n\t\n\n" } }, + { "deletePath": "src/Documents" }, + { "git": ["-C", "src", "add", "-A"] } + ], + "args_extra": ["-ConfigDir", "{workDir}/src", "-Source", "Staged", "-DryRun"], + "expect": { + "stdoutContains": "[note] удаление применится через Configuration.xml: Documents/Товары.xml", + "stdoutNotContains": "[ВНИМАНИЕ] Удаления" + }, + "noSnapshot": "сухой прогон ничего не пишет — проверяется вердикт по удалению" +} diff --git a/tests/skills/cases/db-load-git/error-all-extensions.json b/tests/skills/cases/db-load-git/error-all-extensions.json new file mode 100644 index 000000000..25a953627 --- /dev/null +++ b/tests/skills/cases/db-load-git/error-all-extensions.json @@ -0,0 +1,11 @@ +{ + "name": "Ошибка: -AllExtensions — загрузка частями идёт по одному расширению", + "args_extra": ["-ConfigDir", ".", "-Source", "Staged", "-DryRun", "-AllExtensions"], + "expectError": true, + "expect": { + "stdoutContains": [ + "-AllExtensions cannot be combined with a partial load", + "Загружайте расширения по одному: -Extension <имя>" + ] + } +} diff --git a/tests/skills/cases/db-load-git/error-only-unapplied-deletion.json b/tests/skills/cases/db-load-git/error-only-unapplied-deletion.json new file mode 100644 index 000000000..5b4d106d7 --- /dev/null +++ b/tests/skills/cases/db-load-git/error-only-unapplied-deletion.json @@ -0,0 +1,25 @@ +{ + "name": "Ошибка: в изменениях только удаление без правки состава — загружать нечего", + "preRun": [ + { "writeFile": { "path": "src/Configuration.xml", "content": "\n\t\n\t\t\n\t\t\tТовары\n\t\t\n\t\n\n" } }, + { "writeFile": { "path": "src/Catalogs/Товары.xml", "content": "\n\t\n\t\t\n\t\n\n" } }, + { "writeFile": { "path": "src/Catalogs/Товары/Ext/ObjectModule.bsl", "content": "\n" } }, + { "git": ["-C", "src", "init", "-q"] }, + { "git": ["-C", "src", "add", "-A"] }, + { "git": ["-C", "src", "commit", "-qm", "base"] }, + { "deletePath": "src/Catalogs" }, + { "git": ["-C", "src", "add", "-A"] } + ], + "args_extra": ["-ConfigDir", "{workDir}/src", "-Source", "Staged", "-DryRun"], + "expectError": true, + "expect": { + "stdoutContains": [ + "Catalogs/Товары.xml: владелец Configuration.xml не изменён — состав в базе прежний", + "Error: changes found, but none of them can be applied by a partial load (see above)" + ], + "stdoutNotContains": [ + "No configuration files found in changes", + "ObjectModule.bsl:" + ] + } +} diff --git a/tests/skills/cases/db-load-git/renamed-object-old-path-warns.json b/tests/skills/cases/db-load-git/renamed-object-old-path-warns.json new file mode 100644 index 000000000..579cc1dc1 --- /dev/null +++ b/tests/skills/cases/db-load-git/renamed-object-old-path-warns.json @@ -0,0 +1,19 @@ +{ + "name": "Переименование объекта: старый путь — удаление, о нём предупреждаем (git не прячет его за rename)", + "preRun": [ + { "writeFile": { "path": "src/Configuration.xml", "content": "\n\t\n\t\t\n\t\t\tСтарое\n\t\t\n\t\n\n" } }, + { "writeFile": { "path": "src/Catalogs/Старое.xml", "content": "\n\t\n\t\t\n\t\n\n" } }, + { "git": ["-C", "src", "init", "-q"] }, + { "git": ["-C", "src", "add", "-A"] }, + { "git": ["-C", "src", "commit", "-qm", "base"] }, + { "git": ["-C", "src", "mv", "Catalogs/Старое.xml", "Catalogs/Новое.xml"] } + ], + "args_extra": ["-ConfigDir", "{workDir}/src", "-Source", "Staged", "-DryRun"], + "expect": { + "stdoutContains": [ + "Catalogs/Старое.xml: владелец Configuration.xml не изменён — состав в базе прежний", + " Catalogs/Новое.xml" + ] + }, + "noSnapshot": "сухой прогон ничего не пишет — проверяется вердикт по старому пути" +}