fix(db-load-git): предупреждать об удалениях, которые загрузка частями не применит

Удалённый объект уходит из базы, только если в списке его владелец и в
составе владельца объекта уже нет; удалённый файл-часть (модуль, Ext/)
загрузка частями не удаляет никогда. Скрипт классифицирует удаления:
применимые — [note], остальные — [ВНИМАНИЕ] с причиной; если загружать
нечего, а неприменимые удаления есть — код 1. git diff с --no-renames,
чтобы старый путь переименования был виден. -AllExtensions отвергается
на обоих движках: частями грузится одно расширение за раз.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ANXwbMUhuRFSCsq2RgmTTm
This commit is contained in:
Nick Shirokov
2026-09-24 18:08:20 +03:00
co-authored by Claude Opus 5.5
parent f98e7e9e81
commit 80618f0b93
11 changed files with 385 additions and 31 deletions
+2 -1
View File
@@ -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 <range>` | для Commit | Диапазон коммитов (напр. `HEAD~3..HEAD`) |
| `-Extension <имя>` | нет | Загрузить в расширение |
| `-NoApplyCheck` | нет | Не проверять применимость расширения после загрузки |
| `-AllExtensions` | нет | Загрузить все расширения |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-DryRun` | нет | Только показать что будет загружено (без загрузки) |
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
@@ -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/<ver>/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
}
# Каталоги выгрузки объектов верхнего уровня → элемент в <ChildObjects> конфигурации.
$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)<ChildObjects>.*</ChildObjects>')
$pattern = '(?m)^\s*<' + $tag + '>' + [regex]::Escape($name) + '</' + $tag + '>\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 }
}
+113 -15
View File
@@ -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
# Каталоги выгрузки объектов верхнего уровня → элемент в <ChildObjects> конфигурации.
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"<ChildObjects>.*</ChildObjects>", owner_text, re.S)
pattern = r"(?m)^\s*<" + tag + ">" + re.escape(name) + "</" + tag + r">\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
@@ -0,0 +1,27 @@
{
"name": "Файлы формы удалены, а владелец всё ещё держит её в составе: удаление не применится",
"preRun": [
{ "writeFile": { "path": "src/Configuration.xml", "content": "<MetaDataObject>\n\t<Configuration>\n\t\t<ChildObjects>\n\t\t\t<Catalog>Товары</Catalog>\n\t\t</ChildObjects>\n\t</Configuration>\n</MetaDataObject>\n" } },
{ "writeFile": { "path": "src/Catalogs/Товары.xml", "content": "<MetaDataObject>\n\t<Catalog>\n\t\t<ChildObjects>\n\t\t\t<Form>ФормаЭлемента</Form>\n\t\t</ChildObjects>\n\t</Catalog>\n</MetaDataObject>\n" } },
{ "writeFile": { "path": "src/Catalogs/Товары/Forms/ФормаЭлемента.xml", "content": "<MetaDataObject/>\n" } },
{ "writeFile": { "path": "src/Catalogs/Товары/Forms/ФормаЭлемента/Ext/Form.xml", "content": "<Form/>\n" } },
{ "git": ["-C", "src", "init", "-q"] },
{ "git": ["-C", "src", "add", "-A"] },
{ "git": ["-C", "src", "commit", "-qm", "base"] },
{ "writeFile": { "path": "src/Catalogs/Товары.xml", "content": "<MetaDataObject>\n\t<Catalog>\n\t\t<Comment>правка</Comment>\n\t\t<ChildObjects>\n\t\t\t<Form>ФормаЭлемента</Form>\n\t\t</ChildObjects>\n\t</Catalog>\n</MetaDataObject>\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": "сухой прогон ничего не пишет — проверяется вердикт по удалению"
}
@@ -0,0 +1,25 @@
{
"name": "Форма удалена вместе с правкой владельца: удаление применится через владельца",
"preRun": [
{ "writeFile": { "path": "src/Configuration.xml", "content": "<MetaDataObject>\n\t<Configuration>\n\t\t<ChildObjects>\n\t\t\t<Catalog>Товары</Catalog>\n\t\t</ChildObjects>\n\t</Configuration>\n</MetaDataObject>\n" } },
{ "writeFile": { "path": "src/Catalogs/Товары.xml", "content": "<MetaDataObject>\n\t<Catalog>\n\t\t<ChildObjects>\n\t\t\t<Form>ФормаЭлемента</Form>\n\t\t</ChildObjects>\n\t</Catalog>\n</MetaDataObject>\n" } },
{ "writeFile": { "path": "src/Catalogs/Товары/Forms/ФормаЭлемента.xml", "content": "<MetaDataObject/>\n" } },
{ "writeFile": { "path": "src/Catalogs/Товары/Forms/ФормаЭлемента/Ext/Form.xml", "content": "<Form/>\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": "<MetaDataObject>\n\t<Catalog>\n\t\t<ChildObjects>\n\t\t\t<Attribute>\n\t\t\t\t<Properties>\n\t\t\t\t\t<Name>ФормаЭлемента</Name>\n\t\t\t\t</Properties>\n\t\t\t</Attribute>\n\t\t</ChildObjects>\n\t</Catalog>\n</MetaDataObject>\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": "сухой прогон ничего не пишет — проверяется вердикт по удалению"
}
@@ -0,0 +1,22 @@
{
"name": "Удалённый модуль объекта: загрузка частями его не удалит — [ВНИМАНИЕ]",
"preRun": [
{ "writeFile": { "path": "src/Configuration.xml", "content": "<MetaDataObject>\n\t<Configuration>\n\t\t<ChildObjects>\n\t\t\t<Catalog>Товары</Catalog>\n\t\t</ChildObjects>\n\t</Configuration>\n</MetaDataObject>\n" } },
{ "writeFile": { "path": "src/Catalogs/Товары.xml", "content": "<MetaDataObject>\n\t<Catalog>\n\t\t<ChildObjects/>\n\t</Catalog>\n</MetaDataObject>\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": "сухой прогон ничего не пишет — проверяется вердикт по удалению"
}
@@ -0,0 +1,20 @@
{
"name": "Удалены файлы вне конфигурации (README, docs): не удаления объектов, выход без ошибки",
"preRun": [
{ "writeFile": { "path": "src/Configuration.xml", "content": "<MetaDataObject>\n\t<Configuration>\n\t\t<ChildObjects/>\n\t</Configuration>\n</MetaDataObject>\n" } },
{ "writeFile": { "path": "src/README.md", "content": "readme\n" } },
{ "writeFile": { "path": "src/docs/notes.xml", "content": "<notes/>\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": "сухой прогон ничего не пишет — проверяется, что посторонние удаления не оцениваются"
}
@@ -0,0 +1,20 @@
{
"name": "Удалён документ, а справочник с тем же именем остался: удаление применится (состав сверяется по типу)",
"preRun": [
{ "writeFile": { "path": "src/Configuration.xml", "content": "<MetaDataObject>\n\t<Configuration>\n\t\t<ChildObjects>\n\t\t\t<Catalog>Товары</Catalog>\n\t\t\t<Document>Товары</Document>\n\t\t</ChildObjects>\n\t</Configuration>\n</MetaDataObject>\n" } },
{ "writeFile": { "path": "src/Catalogs/Товары.xml", "content": "<MetaDataObject/>\n" } },
{ "writeFile": { "path": "src/Documents/Товары.xml", "content": "<MetaDataObject/>\n" } },
{ "git": ["-C", "src", "init", "-q"] },
{ "git": ["-C", "src", "add", "-A"] },
{ "git": ["-C", "src", "commit", "-qm", "base"] },
{ "writeFile": { "path": "src/Configuration.xml", "content": "<MetaDataObject>\n\t<Configuration>\n\t\t<ChildObjects>\n\t\t\t<Catalog>Товары</Catalog>\n\t\t</ChildObjects>\n\t</Configuration>\n</MetaDataObject>\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": "сухой прогон ничего не пишет — проверяется вердикт по удалению"
}
@@ -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 <имя>"
]
}
}
@@ -0,0 +1,25 @@
{
"name": "Ошибка: в изменениях только удаление без правки состава — загружать нечего",
"preRun": [
{ "writeFile": { "path": "src/Configuration.xml", "content": "<MetaDataObject>\n\t<Configuration>\n\t\t<ChildObjects>\n\t\t\t<Catalog>Товары</Catalog>\n\t\t</ChildObjects>\n\t</Configuration>\n</MetaDataObject>\n" } },
{ "writeFile": { "path": "src/Catalogs/Товары.xml", "content": "<MetaDataObject>\n\t<Catalog>\n\t\t<ChildObjects/>\n\t</Catalog>\n</MetaDataObject>\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:"
]
}
}
@@ -0,0 +1,19 @@
{
"name": "Переименование объекта: старый путь — удаление, о нём предупреждаем (git не прячет его за rename)",
"preRun": [
{ "writeFile": { "path": "src/Configuration.xml", "content": "<MetaDataObject>\n\t<Configuration>\n\t\t<ChildObjects>\n\t\t\t<Catalog>Старое</Catalog>\n\t\t</ChildObjects>\n\t</Configuration>\n</MetaDataObject>\n" } },
{ "writeFile": { "path": "src/Catalogs/Старое.xml", "content": "<MetaDataObject>\n\t<Catalog>\n\t\t<ChildObjects/>\n\t</Catalog>\n</MetaDataObject>\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": "сухой прогон ничего не пишет — проверяется вердикт по старому пути"
}