Files
cc-1c-skills/.github/skills/db-repo/scripts/db-repo.py
T

1228 lines
65 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# db-repo v1.14 — 1C configuration repository operations
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: движок только 1cv8 — ibcmd работу с хранилищем не поддерживает (нет такого режима).
"""Работа с хранилищем конфигурации 1С.
Захват и помещение объектов, получение изменений, подключение базы к хранилищу,
история версий, администрирование хранилища.
"""
import argparse
import glob
import json
import os
import random
import re
import shutil
import subprocess
import sys
import tempfile
# --- Additional platform arguments ---
# Свои ключи: доп. аргументами нельзя ни подсунуть вторую пакетную операцию, ни подменить
# реквизиты хранилища. Список команд перечислен поимённо — arg_key_match считает совпадением
# только точное имя или имя с не-буквой следом, поэтому общий префикс ключи не покрыл бы.
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs", "/DisableStartupMessages",
"/ConfigurationRepositoryF", "/ConfigurationRepositoryN", "/ConfigurationRepositoryP",
"/ConfigurationRepositoryLock", "/ConfigurationRepositoryUnlock",
"/ConfigurationRepositoryCommit", "/ConfigurationRepositoryUpdateCfg",
"/ConfigurationRepositoryBindCfg", "/ConfigurationRepositoryUnbindCfg",
"/ConfigurationRepositoryDumpCfg", "/ConfigurationRepositoryReport",
"/ConfigurationRepositoryCreate", "/ConfigurationRepositoryAddUser",
"/ConfigurationRepositoryCopyUsers", "/ConfigurationRepositorySetLabel",
"/ConfigurationRepositoryOptimizeData", "/ConfigurationRepositoryClearCache",
"/ConfigurationRepositoryClearLocalCache", "/ConfigurationRepositoryClearGlobalCache",
]
IBCMD_OWNED_KEYS = []
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP", "/ConfigurationRepositoryP", "-Pwd"]
IBCMD_SECRET_KEYS = []
# Известные ключи хранилища — всё, что начинается с /ConfigurationRepository и не совпало с ними,
# это опечатка или усечение. Отдельная проверка нужна потому, что усечённый ключ платформа НЕ
# считает ошибкой: она открывает конфигуратор интерактивно и висит вечно (в /Out только BOM).
REPO_KNOWN_KEYS = [k for k in V8_OWNED_KEYS if k.startswith("/ConfigurationRepository")]
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get("v8path")
if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
d = parent
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter — catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error — reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p):
"""Numeric sort key from version dir name."""
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path:
v8path = _find_project_v8path()
if not v8path:
if os.name == "nt":
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates:
v8path = max(candidates, key=_version_key)
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
else:
print("Error: 1C executable not found. Specify -V8Path")
sys.exit(1)
if os.path.isdir(v8path):
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path):
print(f"Error: 1C executable not found at {v8path}")
sys.exit(1)
return v8path
def decode_platform_bytes(data):
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — the locale
code page (what text=True uses) mangles both."""
if not data:
return ""
try:
return data.decode("utf-8")
except UnicodeDecodeError:
return data.decode("cp866", errors="replace")
def assert_infobase_exists(path):
"""These skills work on a ready infobase. Saying so up front beats the platform's
"Неверные или отсутствующие параметры соединения" after a launch."""
if not path:
return
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
sys.exit(1)
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path — reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}")
sys.exit(1)
return v
def quote_if_needed(token):
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
verbatim, so a token with a space needs quotes of its own."""
if token and (" " in token or "\t" in token) and '"' not in token:
return f'"{token}"'
return token
def run_v8(v8path, arguments):
"""Run 1cv8 in batch mode and capture its console output.
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
escape those quotes, so there the command line is handed over ready-made.
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
частью значения: путь с пробелом платформа не находит («Неопределена информационная
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
File="…") не задеты: у них кавычки внутри токена, а не по краям.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
def strip_framing_quotes(a):
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
# токеном даёт «Неопределена информационная база», а склеенный
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
# без кавычек обе формы работают.
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
return a[1:-1] # "значение" отдельным токеном
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
i = a.index('"')
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
return a # File="…" не трогаем: там кавычки —
# часть синтаксиса строки соединения,
# и с ними на POSIX всё работает
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def print_platform_output(result):
"""Print what the platform wrote to the console as its own labelled block. Silence stays
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
if not text:
return
limit = 65536
if len(text) > limit:
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
print("--- Вывод платформы ---")
print(text)
print("--- End ---")
def describe_exit(code):
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
if code is None:
return ""
win = {
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
}
if code in win:
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
if -64 <= code < 0:
try:
import signal
name = signal.Signals(-code).name
except (ValueError, AttributeError):
name = f"signal {-code}"
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
"the infobase may be left in an inconsistent state; verify it before retrying")
return ""
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def assert_no_truncated_repo_keys(arglist):
for tok in arglist:
if not tok.lower().startswith("/configurationrepository"):
continue
if not any(arg_key_match(tok, k) for k in REPO_KNOWN_KEYS):
print("Error: unknown configuration repository key '%s' — the platform would open "
"the Designer interactively and hang instead of failing" % tok)
sys.exit(1)
def resolve_extra_args(v8_extra, hints):
"""Движок только 1cv8, поэтому ветки ibcmd (в остальных db-* она есть) здесь нет."""
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, "1cv8", hints)
assert_no_truncated_repo_keys(extra)
return extra
# --- Реквизиты хранилища из .v8-project.json ---
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
# и берёт repository оттуда. Тот же приём, что в cf-edit.py (сопоставление по configSrc).
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
if not d:
break
pj = os.path.join(d, ".v8-project.json")
if os.path.isfile(pj):
return pj
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
def same_path(a, b):
if not a or not b:
return False
try:
return os.path.abspath(a).rstrip("\\/").lower() == os.path.abspath(b).rstrip("\\/").lower()
except Exception:
return False
def find_project_database(args):
"""Запись базы в реестре, соответствующая переданному соединению. None, если не найдена."""
pf = _sg_find_v8project(os.getcwd())
if not pf:
return None
try:
with open(pf, encoding="utf-8-sig") as f:
proj = json.load(f)
except Exception:
return None
for db in proj.get("databases") or []:
if args.InfoBasePath and db.get("path") and same_path(db["path"], args.InfoBasePath):
return db
if args.InfoBaseServer and args.InfoBaseRef and db.get("server") and db.get("ref"):
if (db["server"].lower() == args.InfoBaseServer.lower()
and db["ref"].lower() == args.InfoBaseRef.lower()):
return db
return None
def resolve_repository_settings(args):
"""Возвращает dict path/user/password/from_registry. Явные -Repository* сильнее реестра."""
db_rec = find_project_database(args)
rec = None
if db_rec:
if args.Extension:
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
for ext in db_rec.get("extensions") or []:
if (ext.get("name") or "").lower() == args.Extension.lower():
rec = ext.get("repository")
break
else:
rec = db_rec.get("repository")
path = args.RepositoryPath or ((rec or {}).get("path") or None)
user = args.RepositoryUser or ((rec or {}).get("user") or None)
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
pwd = args.RepositoryPassword or ((rec or {}).get("password") or None)
return {
"path": path.strip().strip('"') if path else None,
"user": user,
"password": pwd,
"from_registry": bool(rec and rec.get("path")),
}
def repository_args(repo):
"""Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P."""
a = []
if not repo or not repo.get("path"):
return a
a.append('/ConfigurationRepositoryF"%s"' % repo["path"])
if repo.get("user"):
a.append('/ConfigurationRepositoryN"%s"' % repo["user"])
if repo.get("password"):
a.append('/ConfigurationRepositoryP"%s"' % repo["password"])
return a
# --- Список объектов ---
# Наружу — один формат: плоский список имён, как у db-dump-xml -Mode Partial -Objects.
# Платформенный XML (http://v8.1c.ru/8.3/config/objects) генерируется здесь и модели не показывается.
#
# Нормализовать имена не нужно: платформа принимает и русские, и английские имена типов
# (Catalog.Склады → «Объект захвачен: Справочник.Склады») и подчинённые пути в обеих раскладках.
CONFIG_ROOT_ALIASES = ["Конфигурация", "Configuration"]
# Подчинённые сущности, которые НЕ являются объектами хранилища: в XML владельца они записаны
# вложенным определением, своего файла и UUID у них нет — захватывать нечего. Отдельными объектами
# регистрируются только те, кто записан ССЫЛКОЙ по имени: форма, макет, команда.
# Платформа на такой запрос отвечает «Загруженный список объектов пуст» — без имени и без причины.
INLINE_CHILD_KINDS = [
"Реквизит", "Attribute",
"СтандартныйРеквизит", "StandardAttribute",
"РеквизитАдресации", "AddressingAttribute",
"ТабличнаяЧасть", "TabularSection",
"Измерение", "Dimension",
"Ресурс", "Resource",
"Графа", "Column",
"ЗначениеПеречисления", "EnumValue",
"ПризнакУчета", "ПризнакУчёта", "AccountingFlag",
"ПризнакУчетаСубконто", "ПризнакУчётаСубконто", "ExtDimensionAccountingFlag",
]
def xml_attr(value):
return (value.replace("&", "&amp;").replace("<", "&lt;")
.replace(">", "&gt;").replace('"', "&quot;"))
def requested_objects(args):
"""Плоский список имён из -Objects и/или -ObjectsFile. Пустой список = вся конфигурация."""
out = []
if args.Objects:
out += [s.strip() for s in args.Objects.split(",") if s.strip()]
if args.ObjectsFile:
if not os.path.exists(args.ObjectsFile):
print("Error: -ObjectsFile not found: %s" % args.ObjectsFile)
sys.exit(1)
with open(args.ObjectsFile, encoding="utf-8-sig") as f:
out += [s.strip() for s in f.read().splitlines()
if s.strip() and not s.strip().startswith("#")]
seen = {}
uniq = []
for name in out:
if name.lower() not in seen:
seen[name.lower()] = True
uniq.append(name)
return uniq
def resolve_lockable_objects(names):
"""Заменяет части объекта на сам объект. Это не сужение и не расширение запроса: владелец —
минимально возможная единица захвата для того, что просили."""
out = []
notes = []
for n in names:
segs = n.split(".")
resolved = n
if len(segs) >= 3 and segs[2] in INLINE_CHILD_KINDS:
resolved = "%s.%s" % (segs[0], segs[1])
notes.append("%s -> %s" % (n, resolved))
if resolved not in out:
out.append(resolved)
if notes:
print("[note] части объекта отдельно не захватываются — взят объект-владелец:")
for x in notes:
print(" %s" % x)
return out
def write_objects_list_xml(names, path, with_children):
"""Пишет платформенный XML со списком объектов, возвращает путь к файлу."""
child = "true" if with_children else "false"
lines = ['<Objects xmlns="http://v8.1c.ru/8.3/config/objects" version="1.0">']
for n in names:
if n in CONFIG_ROOT_ALIASES:
# Корень конфигурации — отдельный элемент. Нужен, чтобы добавить или удалить объект:
# без захвата корня частичная загрузка нового объекта не проходит.
lines.append(' <Configuration includeChildObjects="%s"/>' % child)
continue
esc = xml_attr(n)
is_subsystem = bool(re.match(r"^(Подсистема|Subsystem)\.", n))
if is_subsystem and with_children:
# includeChildObjects у подсистемы означает вложенные ПОДСИСТЕМЫ, а её состав
# подтягивает только вложенный <Subsystem> — поэтому под -WithChildren нужны оба.
lines.append(' <Object fullName="%s" includeChildObjects="true">' % esc)
lines.append(' <Subsystem includeObjectsFromSubordinateSubsystems="true"/>')
lines.append(' </Object>')
else:
lines.append(' <Object fullName="%s" includeChildObjects="%s"/>' % (esc, child))
lines.append('</Objects>')
with open(path, "w", encoding="utf-8-sig", newline="\n") as f:
f.write("\n".join(lines) + "\n")
return path
# --- Разбор лога операции ---
# Платформа отчитывается ПОСТРОЧНО, и код возврата про фактический результат не говорит:
# захват не атомарен (код 1 при реально захваченном объекте), а все no-op'ы дают код 0.
def read_repo_log(log_text):
r = {
"locked": [], "locked_by_other": [], "received": [], "committed": [],
"unchanged": [], "unlocked": [], "not_locked": [], "modified": [], "missing": [],
"has_operation_block": False, "raw": log_text,
}
if not log_text:
return r
in_missing = False
for raw in log_text.splitlines():
line = raw.strip()
if not line:
in_missing = False
continue
if re.match(r"^-+\s*Начало операции с хранилищем", line):
r["has_operation_block"] = True
in_missing = False
continue
if re.match(r"^-+\s*Операция с хранилищем", line):
in_missing = False
continue
if line.startswith("Объекты, отсутствующие в обеих конфигурациях"):
in_missing = True
continue
if in_missing:
r["missing"].append(line)
continue
m = re.match(r"^Объект захвачен для редактирования другим пользователем:\s*(.+?)\s*\((.+?)\)\s*$", line)
if m:
r["locked_by_other"].append({"name": m.group(1), "holder": m.group(2)})
continue
for pattern, key in (
(r"^Объект захвачен для редактирования:\s*(.+)$", "locked"),
(r"^Объект получен из хранилища:\s*(.+)$", "received"),
(r"^Объект помещен в хранилище:\s*(.+)$", "committed"),
(r"^Объект не был изменен:\s*(.+)$", "unchanged"),
(r"^Захват объекта отменен:\s*(.+)$", "unlocked"),
(r"^Объект не захвачен для редактирования:\s*(.+)$", "not_locked"),
(r"^Объект '(.+?)' был изменен", "modified"),
):
m = re.match(pattern, line)
if m:
r[key].append(m.group(1).strip())
break
return r
# --- Полученные объекты → готовая команда перевыгрузки ---
# Захват и обновление МОЛЧА подтягивают свежие версии в локальную конфигурацию. Если после этого
# загрузить старые исходники, чужие изменения откатятся без единой ошибки. Выгрузку не делаем сами
# (она затрёт локальные правки) — печатаем готовую команду.
def owner_objects(names):
owners = []
for n in names:
parts = n.split(".")
if len(parts) == 1:
continue # корень конфигурации: частично по имени не выгружается
owner = "%s.%s" % (parts[0], parts[1]) if len(parts) > 2 else n
if owner not in owners:
owners.append(owner)
return owners
def print_received_warning(received):
if not received:
return
print("")
print("[warning] локальная конфигурация изменена, получено объектов из хранилища: %d" % len(received))
for n in received[:LIST_LIMIT]:
print(" %s" % n)
if len(received) > LIST_LIMIT:
print(" … и ещё %d" % (len(received) - LIST_LIMIT))
owners = owner_objects(received)
has_root = any(len(n.split(".")) == 1 for n in received)
if owners:
list_path = os.path.join(tempfile.gettempdir(), "db-repo-received.txt")
with open(list_path, "w", encoding="utf-8-sig", newline="\n") as f:
f.write("\n".join(owners) + "\n")
print("Исходники в проекте устарели по этим объектам. Перевыгрузите их ПЕРЕД правкой,")
print("иначе частичная загрузка старых исходников молча откатит чужие изменения:")
print(' /db-dump-xml <база> <каталог> -Mode Partial -ObjectsFile "%s"' % list_path)
if has_root:
print("Получен корень конфигурации — по имени он частично не выгружается,")
print("используйте /db-dump-xml -Mode Changes.")
# --- Подкоманды ---
# Ключ выбирается по таблице: произвольный ключ задать нельзя. Неизвестная подкоманда — ошибка,
# а не проброс: платформа на неизвестный ключ открывает конфигуратор и висит.
COMMAND_KEYS = {
"lock": "/ConfigurationRepositoryLock",
"unlock": "/ConfigurationRepositoryUnlock",
"commit": "/ConfigurationRepositoryCommit",
"update": "/ConfigurationRepositoryUpdateCfg",
"connect": "/ConfigurationRepositoryBindCfg",
"disconnect": "/ConfigurationRepositoryUnbindCfg",
"report": "/ConfigurationRepositoryReport",
"dump-cfg": "/ConfigurationRepositoryDumpCfg",
"create": "/ConfigurationRepositoryCreate",
"add-user": "/ConfigurationRepositoryAddUser",
"copy-users": "/ConfigurationRepositoryCopyUsers",
"set-label": "/ConfigurationRepositorySetLabel",
"optimize": "/ConfigurationRepositoryOptimizeData",
"clear-cache": "/ConfigurationRepositoryClearCache",
}
# Прощающий ввод: синонимы намеренно НЕ документируются, в SKILL.md одна каноничная форма.
COMMAND_ALIASES = {
"capture": "lock", "захватить": "lock",
"release": "unlock", "отменить-захват": "unlock",
"put": "commit", "поместить": "commit",
"pull": "update", "получить": "update", "update-cfg": "update",
"bind": "connect",
"dumpcfg": "dump-cfg", "dump": "dump-cfg",
"adduser": "add-user", "copyusers": "copy-users", "setlabel": "set-label",
"clearcache": "clear-cache", "optimize-data": "optimize",
}
def resolve_command(raw):
c = raw.strip().lower()
# unbind отличается от unlock одной буквой, а последствия разные: отмена захвата против
# отключения базы от хранилища. Молча угадывать нельзя.
if c == "unbind":
print("Error: 'unbind' is ambiguous — did you mean 'unlock' (release captured objects) "
"or 'disconnect' (detach the base from the repository)?")
sys.exit(1)
c = COMMAND_ALIASES.get(c, c)
if c not in COMMAND_KEYS:
print("Error: unknown command '%s'. Known: %s" % (raw, ", ".join(sorted(COMMAND_KEYS))))
sys.exit(1)
return c
# --- Вердикт ---
# Код возврата платформы про фактический результат не говорит: захват и помещение НЕ атомарны
# (код 1 при реально захваченном объекте), а все no-op'ы дают код 0. Меряем не «изменилось ли»,
# а «достигнуто ли запрошенное»: достигнуто, в том числе уже было — 0; частично — 0 с поимённым
# предупреждением; не достигнуто ничего — 1.
def object_key(name):
"""Ключ для сопоставления запрошенного с тем, что назвала платформа. Имена типов в паре
«запрос ↔ лог» бывают на разных языках: платформа ПРИНИМАЕТ Catalog.Номенклатура, а
ОТВЕЧАЕТ всегда Справочник.Номенклатура. Поэтому сравниваем только собственные имена —
нечётные сегменты пути; они в обеих раскладках одинаковы, и карта типов не нужна.
Цена приёма: Справочник.Х и Документ.Х дают один ключ. Влияет только на формулировку
вердикта, не на выполненную операцию."""
segs = name.split(".")
own = segs[1::2]
if not own:
own = [segs[0]]
return ".".join(own).lower()
# Операция над всей конфигурацией перечисляет тысячи объектов. Печатаем начало списка,
# остальное кладём в файл: модели нужен факт и путь, а не простыня.
LIST_LIMIT = 20
def save_object_list(names, key):
if not key:
key = "objects"
path = os.path.join(tempfile.gettempdir(), "db-repo-%s.txt" % key)
with open(path, "w", encoding="utf-8-sig", newline="\n") as f:
f.write("\n".join(names) + "\n")
return path
def print_repo_objects(title, names, file_key="objects"):
if not names:
return
print("%s (%d):" % (title, len(names)))
for n in names[:LIST_LIMIT]:
print(" %s" % n)
if len(names) > LIST_LIMIT:
path = save_object_list(names, file_key)
print(" … и ещё %d; полный список: %s" % (len(names) - LIST_LIMIT, path))
def write_repo_verdict(cmd, log, platform_exit, requested, report_format="", output_file=""):
if log["missing"]:
print("Error: objects not found in the configuration:")
for n in log["missing"][:LIST_LIMIT]:
print(" %s" % n)
if len(log["missing"]) > LIST_LIMIT:
print(" … и ещё %d; полный список: %s"
% (len(log["missing"]) - LIST_LIMIT, save_object_list(log["missing"], "missing")))
print("Возможные причины:")
print(" - опечатка в имени. Принимаются обе формы: Справочник.Номенклатура и Catalog.Номенклатура")
print(" - база отстала от хранилища, объект появился позже — выполните /db-repo update")
print(" - это не объект хранилища: реквизиты, табличные части, измерения и ресурсы")
print(" отдельно не захватываются — указывайте объект-владельца")
return 1
if cmd == "lock":
# Порядок вывода: факты, затем вердикт, и только потом совет. Совет длинный, и
# между фактами и вердиктом он прятал бы главную строку.
print_repo_objects("Захвачено", log["locked"], "locked")
if log["locked_by_other"]:
print("Не удалось захватить (%d):" % len(log["locked_by_other"]))
for o in log["locked_by_other"][:LIST_LIMIT]:
print(" %s — держит %s" % (o["name"], o["holder"]))
if len(log["locked_by_other"]) > LIST_LIMIT:
path = save_object_list(
["%s%s" % (o["name"], o["holder"]) for o in log["locked_by_other"]], "blocked")
print(" … и ещё %d; полный список: %s"
% (len(log["locked_by_other"]) - LIST_LIMIT, path))
if not log["locked"] and log["locked_by_other"]:
# «Уже захвачено мной» платформа не печатает вовсе, поэтому отличить его от
# «не захвачено» по логу нельзя — сверяем с тем, что просили. Заняты ВСЕ
# запрошенные объекты только тогда, когда каждый из них назван занятым.
blocked = {object_key(o["name"]) for o in log["locked_by_other"]}
asked = [object_key(n) for n in requested]
all_blocked = (not asked) or all(k in blocked for k in asked)
if all_blocked:
print("Захват не выполнен: все запрошенные объекты заняты.")
print_received_warning(log["received"])
return 1
print("[warning] часть запрошенного занята другими; остальное уже было захвачено вами.")
print(" Захваченное можно править: код возврата 0 именно поэтому.")
print_received_warning(log["received"])
return 0
if not log["locked"] and platform_exit == 0:
# Захват уже захваченного СОБОЙ: платформа не печатает ни блока операции, ни строк.
print("Объекты уже захвачены вами — изменений не потребовалось.")
print_received_warning(log["received"])
return 0
if not log["locked"]:
print("Захват не выполнен (код %s)%s" % (platform_exit, describe_exit(platform_exit)))
print_received_warning(log["received"])
return 1
if log["locked_by_other"]:
print("[warning] захват выполнен частично — перечисленные выше объекты остались у других пользователей.")
print(" Захваченное можно править: код возврата 0 именно поэтому.")
print_received_warning(log["received"])
return 0
print_received_warning(log["received"])
return platform_exit
if cmd == "unlock":
# Отмена захвата атомарна: при локальных изменениях платформа отменяет операцию целиком.
if platform_exit != 0:
if log["modified"]:
print("Отмена захвата не выполнена: у объектов есть локальные изменения (%d):" % len(log["modified"]))
for n in log["modified"][:LIST_LIMIT]:
print(" %s" % n)
if len(log["modified"]) > LIST_LIMIT:
print(" … и ещё %d; полный список: %s"
% (len(log["modified"]) - LIST_LIMIT, save_object_list(log["modified"], "modified")))
print("Поместите их (/db-repo commit) либо откажитесь от них: -Force перезапишет "
"объекты версией из хранилища.")
else:
print("Отмена захвата не выполнена (код %s)%s" % (platform_exit, describe_exit(platform_exit)))
return 1
print_repo_objects("Захват отменён", log["unlocked"], "unlocked")
print_repo_objects("Не были захвачены — снимать нечего", log["not_locked"], "not-locked")
if not log["unlocked"]:
print("Изменений не потребовалось.")
print_received_warning(log["received"])
return 0
if cmd == "commit":
if platform_exit != 0:
# Платформа отдаёт голую «Ошибка помещения изменений объектов в хранилище» —
# ни объекта, ни причины, одинаково для всех причин. Диагностику даём свою.
print("Помещение не выполнено (код %s)%s" % (platform_exit, describe_exit(platform_exit)))
print("Платформа не называет причину. Проверьте:")
print(" - захвачен ли объект вами: /db-repo lock")
print(" - не держит ли его другой пользователь")
print(" - есть ли у пользователя хранилища право на помещение")
return 1
print_repo_objects("Помещено в хранилище", log["committed"], "committed")
print_repo_objects("Без изменений — не помещались", log["unchanged"], "unchanged")
if not log["committed"]:
print("Новая версия в хранилище НЕ создана: помещать было нечего.")
return 0
if cmd == "update":
if platform_exit != 0:
print("Получение изменений не выполнено (код %s)%s" % (platform_exit, describe_exit(platform_exit)))
return 1
if not log["has_operation_block"]:
# У подключённой базы лог всегда содержит блок операции — даже когда получать нечего.
# Его отсутствие означает, что база к хранилищу НЕ подключена, а тогда эта команда
# молча заменяет всю конфигурацию содержимым хранилища и рапортует успех.
print("Error: the platform reported success but printed no repository operation block.")
print(" Похоже, база НЕ подключена к хранилищу — в этом случае команда заменяет")
print(" всю конфигурацию базы содержимым хранилища. Проверьте состояние базы.")
return 1
print_repo_objects("Получено из хранилища", log["received"], "received")
if not log["received"]:
print("Изменений в хранилище нет — конфигурация уже актуальна.")
return 0
print_received_warning(log["received"])
return 0
if platform_exit != 0:
print("Команда '%s' завершилась с ошибкой (код %s)%s"
% (cmd, platform_exit, describe_exit(platform_exit)))
# Подключение базы, у которой конфигурация уже есть, платформа отклоняет без
# подсказки — а действие ровно одно, и оно необратимо.
if "связанная с данным хранилищем" in (log.get("raw") or ""):
print("[hint] за этим пользователем хранилища уже числится другая база.")
print(" Подключить всё равно — -ForceBindAlreadyBindedUser.")
if "Конфигурация не пустая" in (log.get("raw") or ""):
print("[hint] в базе уже есть конфигурация. Замена её конфигурацией из хранилища —")
print(" -ForceReplaceCfg. Операция необратима: спросите подтверждение у пользователя.")
print(" Если база уже была подключена, понадобится ещё -ForceBindAlreadyBindedUser.")
return 1
print("Команда '%s' выполнена." % cmd)
if cmd == "report" and report_format == "txt" and os.path.exists(output_file):
with open(output_file, encoding="utf-8-sig") as f:
lines = f.read().rstrip().splitlines()
# Короткий отчёт печатаем целиком, длинный — не печатаем ВОВСЕ. Обрезанный отчёт
# по версиям опаснее длинного: он читается как полный ответ, и по куску легко
# заключить «объект не менялся». Список объектов при обрезке хотя бы очевидно неполон.
if len(lines) <= 100:
print("--- %s ---" % output_file)
print(chr(10).join(lines))
print("--- End ---")
else:
print("Отчёт не печатается целиком (строк: %d): %s" % (len(lines), output_file))
print("Сузьте выборку: -NBegin -1 (только последняя версия), -NBegin/-NEnd (диапазон),")
print("-DateBegin/-DateEnd (период), -GroupByObject (сводка по объектам).")
return 0
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(
description="1C configuration repository operations",
allow_abbrev=False,
)
parser.add_argument("-Command", default="")
parser.add_argument("-V8Path", default="")
parser.add_argument("-InfoBasePath", default="")
parser.add_argument("-InfoBaseServer", default="")
parser.add_argument("-InfoBaseRef", default="")
parser.add_argument("-UserName", default="")
parser.add_argument("-Password", default="")
parser.add_argument("-RepositoryPath", default="")
parser.add_argument("-RepositoryUser", default="")
parser.add_argument("-RepositoryPassword", default="")
parser.add_argument("-Extension", default="")
parser.add_argument("-Objects", default="")
parser.add_argument("-ObjectsFile", default="")
parser.add_argument("-WithChildren", action="store_true")
# Операция над ВСЕЙ конфигурацией. Отдельный флаг, а не умолчание при забытом -Objects:
# захват всей конфигурации на большой базе идёт долго и блокирует работу всей команде.
parser.add_argument("-All", dest="All", action="store_true")
parser.add_argument("-Comment", default="")
parser.add_argument("-KeepLocked", action="store_true")
parser.add_argument("-Force", action="store_true")
parser.add_argument("-Revised", action="store_true")
parser.add_argument("-Version", dest="Version", default="")
parser.add_argument("-OutputFile", default="")
# --- report ---
parser.add_argument("-NBegin", default="")
parser.add_argument("-NEnd", default="")
parser.add_argument("-DateBegin", default="")
parser.add_argument("-DateEnd", default="")
parser.add_argument("-GroupByObject", action="store_true")
parser.add_argument("-GroupByComment", action="store_true")
parser.add_argument("-ReportFormat", default="txt", choices=["txt", "mxl"])
# --- users ---
parser.add_argument("-NewUser", default="")
parser.add_argument("-NewUserPassword", default="")
parser.add_argument("-Rights", default="", choices=[
"", "ReadOnly", "LockObjects", "ManageConfigurationVersions", "Administration"])
parser.add_argument("-RestoreDeletedUser", action="store_true")
parser.add_argument("-SourcePath", default="")
parser.add_argument("-SourceUser", default="")
parser.add_argument("-SourcePassword", default="")
# --- create / connect ---
parser.add_argument("-NoBind", action="store_true")
parser.add_argument("-AllowConfigurationChanges", action="store_true")
parser.add_argument("-ChangesAllowedRule", default="", choices=[
"", "ObjectNotEditable", "ObjectIsEditableSupportEnabled", "ObjectNotSupported"])
parser.add_argument("-ChangesNotRecommendedRule", default="", choices=[
"", "ObjectNotEditable", "ObjectIsEditableSupportEnabled", "ObjectNotSupported"])
parser.add_argument("-ForceReplaceCfg", action="store_true")
parser.add_argument("-ForceBindAlreadyBindedUser", action="store_true")
parser.add_argument("-Label", default="")
parser.add_argument("-CacheScope", default="local", choices=["local", "global", "db"])
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, _ = extract_extra_args(sys.argv[1:], known_opts)
args = ci_parse_args(parser, argv)
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
assert_infobase_exists(args.InfoBasePath)
v8path = resolve_v8path(args.V8Path)
if not args.Command:
print("Error: -Command is required. Known: %s" % ", ".join(sorted(COMMAND_KEYS)))
sys.exit(1)
cmd = resolve_command(args.Command)
if not args.InfoBasePath and not (args.InfoBaseServer and args.InfoBaseRef):
print("Error: specify -InfoBasePath, or -InfoBaseServer together with -InfoBaseRef")
sys.exit(1)
repo = resolve_repository_settings(args)
if not repo["path"]:
if args.Extension:
print("Error: repository path for extension '%s' is unknown — add extensions[].repository "
"to the database record in .v8-project.json, or pass -RepositoryPath" % args.Extension)
print(" Note: an extension has its OWN repository with its own path.")
else:
print("Error: repository path is unknown — add repository to the database record "
"in .v8-project.json, or pass -RepositoryPath")
sys.exit(1)
# Подтверждение деструктивных операций спрашивает модель (см. SKILL.md) — так же, как у полной
# загрузки в db-load-xml. Здесь остаётся только то, что моделью не проверяется: команда update на
# базе, про которую реестр не знает, что она подключена к хранилищу. На НЕподключённой базе
# UpdateCfg молча заменяет всю конфигурацию содержимым хранилища и рапортует успех.
if cmd == "update" and not repo["from_registry"] and not args.RepositoryPath:
print("Error: the database record in .v8-project.json declares no repository — refusing to run update")
print(" On a base NOT bound to a repository this command silently REPLACES the whole configuration")
print(" with the repository content and still reports success.")
sys.exit(1)
object_aware = ["lock", "unlock", "commit", "update"]
requested = resolve_lockable_objects(requested_objects(args))
if requested and cmd not in object_aware:
print("Error: -Objects/-ObjectsFile does not apply to '%s'" % cmd)
sys.exit(1)
# Операции над всей конфигурацией требуют явного -All. Умолчанием это быть не может: захват
# всей конфигурации на большой базе идёт долго и блокирует работу всей команде, а получается
# такой вызов от одного забытого -Objects.
whole_config_aware = ["lock", "unlock", "commit"]
if args.All and requested:
print("Error: -All (whole configuration) contradicts -Objects/-ObjectsFile — pick one")
sys.exit(1)
if cmd in whole_config_aware and not requested and not args.All:
print("Error: '%s' without -Objects would affect the WHOLE configuration." % cmd)
print(' Укажите объекты: -Objects "Справочник.Номенклатура"')
print(" Это и нужно — подтвердите явно: -All")
sys.exit(1)
# Корень с подчинёнными — это тоже вся конфигурация, хотя выглядит как «захвачу корень».
if args.WithChildren:
root_asked = [n for n in requested if n in CONFIG_ROOT_ALIASES]
if root_asked:
print("Error: '%s' with -WithChildren means the WHOLE configuration, not just its root." % root_asked[0])
print(" Нужен только корень (чтобы добавить или удалить объект) — уберите -WithChildren.")
print(" Нужна вся конфигурация — вызовите с -All вместо -Objects.")
sys.exit(1)
if cmd == "report" and not args.OutputFile:
# Отчёт печатается в вывод, поэтому путь нужен только если его хотят сохранить.
args.OutputFile = os.path.join(tempfile.gettempdir(), "db-repo-report.%s" % args.ReportFormat)
if cmd == "dump-cfg" and not args.OutputFile:
print("Error: -OutputFile (path to the .cf file) is required for dump-cfg")
sys.exit(1)
if cmd == "add-user" and (not args.NewUser or not args.Rights):
print("Error: -NewUser and -Rights are required for add-user")
sys.exit(1)
if cmd == "copy-users" and (not args.SourcePath or not args.SourceUser):
print("Error: -SourcePath and -SourceUser are required for copy-users")
sys.exit(1)
if cmd == "set-label" and not args.Label:
print("Error: -Label is required for set-label")
sys.exit(1)
arg_hints = {
"/F": "-InfoBasePath",
"/S": "-InfoBaseServer + -InfoBaseRef",
"/N": "-UserName",
"/P": "-Password",
}
extra_args = resolve_extra_args(v8_extra, arg_hints)
temp_dir = os.path.join(tempfile.gettempdir(), "db_repo_%d" % random.randint(1, 2 ** 31))
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Соединение ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", '"%s/%s"' % (args.InfoBaseServer, args.InfoBaseRef)]
else:
arguments += ["/F", '"%s"' % args.InfoBasePath]
if args.UserName:
arguments.append('/N"%s"' % args.UserName)
if args.Password:
arguments.append('/P"%s"' % args.Password)
arguments += repository_args(repo)
# --- Команда ---
key = COMMAND_KEYS[cmd]
if cmd == "clear-cache":
key = {"local": "/ConfigurationRepositoryClearLocalCache",
"global": "/ConfigurationRepositoryClearGlobalCache",
"db": "/ConfigurationRepositoryClearCache"}[args.CacheScope]
arguments.append(key)
if cmd in ("dump-cfg", "report"):
arguments.append('"%s"' % args.OutputFile)
if cmd in object_aware and requested:
objects_xml = write_objects_list_xml(
requested, os.path.join(temp_dir, "objects.xml"), args.WithChildren)
arguments += ["-Objects", '"%s"' % objects_xml]
if cmd == "lock":
if args.Revised:
arguments.append("-revised")
elif cmd == "unlock":
if args.Force:
arguments.append("-force")
elif cmd == "commit":
if args.Comment:
# Многострочный комментарий задаётся своим -comment на каждую строку.
for line in args.Comment.splitlines():
arguments += ["-comment", '"%s"' % line]
if args.KeepLocked:
arguments.append("-keepLocked")
if args.Force:
arguments.append("-force")
elif cmd == "update":
if args.Version:
arguments += ["-v", args.Version]
if args.Revised:
arguments.append("-revised")
if args.Force:
arguments.append("-force")
elif cmd == "connect":
if args.ForceBindAlreadyBindedUser:
arguments.append("-forceBindAlreadyBindedUser")
if args.ForceReplaceCfg:
arguments.append("-forceReplaceCfg")
elif cmd == "disconnect":
if args.Force:
arguments.append("-force")
elif cmd == "dump-cfg":
if args.Version:
arguments += ["-v", args.Version]
elif cmd == "report":
if args.NBegin:
arguments += ["-NBegin", args.NBegin]
if args.NEnd:
arguments += ["-NEnd", args.NEnd]
if args.DateBegin:
arguments += ["-DateBegin", '"%s"' % args.DateBegin]
if args.DateEnd:
arguments += ["-DateEnd", '"%s"' % args.DateEnd]
if args.GroupByObject:
arguments.append("-GroupByObject")
if args.GroupByComment:
arguments.append("-GroupByComment")
arguments += ["-ReportFormat", args.ReportFormat]
elif cmd == "create":
if args.AllowConfigurationChanges:
arguments.append("-AllowConfigurationChanges")
if args.ChangesAllowedRule:
arguments += ["-ChangesAllowedRule", args.ChangesAllowedRule]
if args.ChangesNotRecommendedRule:
arguments += ["-ChangesNotRecommendedRule", args.ChangesNotRecommendedRule]
if args.NoBind:
arguments.append("-NoBind")
elif cmd == "add-user":
arguments += ["-User", '"%s"' % args.NewUser]
if args.NewUserPassword:
arguments += ["-Pwd", '"%s"' % args.NewUserPassword]
arguments += ["-Rights", args.Rights]
if args.RestoreDeletedUser:
arguments.append("-RestoreDeletedUser")
elif cmd == "copy-users":
arguments += ["-Path", '"%s"' % args.SourcePath]
arguments += ["-User", '"%s"' % args.SourceUser]
if args.SourcePassword:
arguments += ["-Pwd", '"%s"' % args.SourcePassword]
if args.RestoreDeletedUser:
arguments.append("-RestoreDeletedUser")
elif cmd == "set-label":
if args.Version:
arguments += ["-v", args.Version]
arguments += ["-name", '"%s"' % args.Label]
if args.Comment:
for line in args.Comment.splitlines():
arguments += ["-comment", '"%s"' % line]
if args.Extension:
arguments += ["-Extension", '"%s"' % args.Extension]
log_file = os.path.join(temp_dir, "repo_log.txt")
arguments += ["/Out", '"%s"' % log_file]
arguments.append("/DisableStartupDialogs")
arguments.append("/DisableStartupMessages")
arguments += extra_args
secrets = [args.Password, repo["password"], args.NewUserPassword, args.SourcePassword]
print("Running: 1cv8.exe %s"
% _redact(" ".join(format_args_for_display(arguments, "1cv8")), *secrets))
result = run_v8(v8path, arguments)
exit_code = result.returncode
log_text = ""
if os.path.exists(log_file):
# /Out — всегда UTF-8 с BOM.
with open(log_file, encoding="utf-8-sig") as f:
log_text = f.read()
log = read_repo_log(log_text)
verdict = write_repo_verdict(cmd, log, exit_code, requested,
args.ReportFormat, args.OutputFile)
# Разбор мог не покрыть причину (у commit её вовсе нет в логе) — при отказе показываем сырой лог.
if verdict != 0 and log_text.strip():
# Лог операции над всей конфигурацией — тысячи строк. Показываем хвост: итог и причина
# отказа платформа пишет в конце.
log_lines = log_text.rstrip().splitlines()
log_limit = 200
print("--- Log ---")
if len(log_lines) > log_limit:
print("[... показаны последние %d строк из %d ...]" % (log_limit, len(log_lines)))
log_lines = log_lines[-log_limit:]
print("\n".join(log_lines))
print("--- End ---")
# Реестр — не формальность: без repository реквизиты придётся передавать в каждом вызове,
# а update откажется работать вовсе. Модель об этом не вспомнит, поэтому даём готовый блок.
# «Соединение не установлено» у сетевого хранилища означает не отсутствие реквизитов,
# а недоступный сервер — трактовка «добавьте repository» увела бы не туда.
if verdict != 0 and "Соединение с хранилищем конфигурации не установлено" in log_text:
if re.match(r"^tcp://", repo["path"] or ""):
print("[hint] сервер хранилища недоступен по адресу %s." % repo["path"])
print(" Проверьте, запущен ли сервер хранилища и верен ли порт (по умолчанию 1542).")
elif re.match(r"^https?://", repo["path"] or ""):
print("[hint] хранилище недоступно по адресу %s." % repo["path"])
print(" Проверьте веб-сервер и публикацию хранилища.")
else:
print("[hint] хранилище недоступно по пути %s — проверьте путь и реквизиты."
% repo["path"])
# Признак — что путь задали аргументом: значит в реестре его нет (или он другой).
if verdict == 0 and cmd in ("create", "connect") and args.RepositoryPath:
json_path = repo["path"].replace("\\", "\\\\")
print("")
print("[note] допишите хранилище в запись базы в .v8-project.json (см. /db-list):")
if args.Extension:
print(' "extensions": [ { "name": "%s", "repository": { "path": "%s", "user": "%s", '
'"password": "<пароль>" } } ]' % (args.Extension, json_path, repo["user"] or ""))
else:
print(' "repository": { "path": "%s", "user": "%s", "password": "<пароль>" }'
% (json_path, repo["user"] or ""))
print_platform_output(result)
sys.exit(verdict)
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()