From b43d3b11c356aaba38c1fd0a15f422cf5e030706 Mon Sep 17 00:00:00 2001 From: MarkWuRY168 Date: Sat, 13 Jun 2026 22:32:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20sap-cli=20skill=20v2.2.0=20=E2=80=94=20?= =?UTF-8?q?upload=20+=20syntax-check=20commands?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SKILL.md | 2 +- VERSION | 2 +- assets/sapcli/__init__.py | 2 +- assets/sapcli/cli/app.py | 4 + assets/sapcli/cli/parser.py | 12 +++ assets/sapcli/commands/__init__.py | 8 ++ assets/sapcli/commands/syntax_check.py | 61 +++++++++++++++ assets/sapcli/commands/upload.py | 104 +++++++++++++++++++++++++ 8 files changed, 192 insertions(+), 3 deletions(-) create mode 100644 assets/sapcli/commands/syntax_check.py create mode 100644 assets/sapcli/commands/upload.py diff --git a/SKILL.md b/SKILL.md index eb96056..375c72b 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: sap-cli description: "Use when you need to operate SAP ABAP development objects — syncing code, querying metadata, managing transports, reading tables, or any task involving SAP ADT REST API. Trigger on: ABAP objects, SAP development, transport requests, DDIC types, SE11/SE16N/SE38/SE80 equivalents, 'sync to SAP', 'download from SAP', 'activate ABAP', 'read SAP table'. NOT for developing sap-cli itself." -version: "2.1.0" +version: "2.2.0" author: WuRangyu license: MIT --- diff --git a/VERSION b/VERSION index 7ec1d6d..ccbccc3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.1.0 +2.2.0 diff --git a/assets/sapcli/__init__.py b/assets/sapcli/__init__.py index c82081f..e8fb274 100644 --- a/assets/sapcli/__init__.py +++ b/assets/sapcli/__init__.py @@ -2,4 +2,4 @@ sap-cli — 基于 SAP ADT REST API 的 ABAP 开发对象管理工具 """ -__version__ = "2.1.0" +__version__ = "2.2.0" diff --git a/assets/sapcli/cli/app.py b/assets/sapcli/cli/app.py index 3b3ad11..62806d2 100644 --- a/assets/sapcli/cli/app.py +++ b/assets/sapcli/cli/app.py @@ -35,6 +35,8 @@ from sapcli.commands import ( cmd_read_table, cmd_run_program, cmd_activate, + cmd_upload, + cmd_syntax_check, ) from sapcli.auth import cmd_auth_login, cmd_auth_logout, cmd_auth_status from sapcli.exceptions import SapCliError, ConfigError @@ -134,6 +136,8 @@ def main() -> None: "check": cmd_check, "format": cmd_format, "activate": cmd_activate, + "upload": cmd_upload, + "syntax-check": cmd_syntax_check, "diff": cmd_diff, "package": cmd_package, "cds": cmd_cds, diff --git a/assets/sapcli/cli/parser.py b/assets/sapcli/cli/parser.py index 598b4aa..13a534f 100644 --- a/assets/sapcli/cli/parser.py +++ b/assets/sapcli/cli/parser.py @@ -178,6 +178,18 @@ CDS View: act_parser.add_argument("--types", default=None, help="逗号分隔的多个对象类型(批量模式,与 --names 一一对应)") act_parser.add_argument("--corr_nr", default=None, help="传输请求编号") + # ── upload (仅上传源码,不检查不激活) ── + upload_parser = subparsers.add_parser("upload", help="上传源代码到 SAP(不检查不激活)") + upload_parser.add_argument("--name", required=True, help="对象名称 (function 类型用'组名/模块名')") + upload_parser.add_argument("--type", required=True, choices=type_choices, help="对象类型") + upload_parser.add_argument("--path", required=True, help="本地源代码文件路径 (.abap)") + upload_parser.add_argument("--corr_nr", default=None, help="传输请求编号") + + # ── syntax-check (远程对象语法检查,不上传不激活) ── + sc_parser = subparsers.add_parser("syntax-check", help="语法检查(远程对象,不上传不激活)") + sc_parser.add_argument("--name", required=True, help="对象名称") + sc_parser.add_argument("--type", required=True, choices=type_choices, help="对象类型") + # ── diff (代码差异对比) ── diff_parser = subparsers.add_parser("diff", help="本地 vs SAP 代码差异对比") diff_parser.add_argument("--name", required=True, help="对象名称") diff --git a/assets/sapcli/commands/__init__.py b/assets/sapcli/commands/__init__.py index cd75340..cff82db 100644 --- a/assets/sapcli/commands/__init__.py +++ b/assets/sapcli/commands/__init__.py @@ -54,6 +54,12 @@ from sapcli.commands.program_run import ( from sapcli.commands.activate import ( cmd_activate, ) +from sapcli.commands.upload import ( + cmd_upload, +) +from sapcli.commands.syntax_check import ( + cmd_syntax_check, +) __all__ = [ "cmd_create", @@ -80,4 +86,6 @@ __all__ = [ "cmd_read_table", "cmd_run_program", "cmd_activate", + "cmd_upload", + "cmd_syntax_check", ] diff --git a/assets/sapcli/commands/syntax_check.py b/assets/sapcli/commands/syntax_check.py new file mode 100644 index 0000000..1211d41 --- /dev/null +++ b/assets/sapcli/commands/syntax_check.py @@ -0,0 +1,61 @@ +"""syntax-check 命令 — 对 SAP 远程对象执行语法检查(不上传不激活)。""" +from __future__ import annotations + +import argparse +import logging + +from sapcli.client import ADTClient +from sapcli.exceptions import InvalidNameError, ObjectNotFoundError +from sapcli.types import get_type_config, parse_object_name + +logger = logging.getLogger("sapcli.commands.syntax_check") + + +def cmd_syntax_check(args: argparse.Namespace, client: ADTClient) -> None: + """对 SAP 远程对象执行语法检查(不上传代码、不激活)。 + + 检查 SAP 上现有对象的源码(通常是 active 状态),用于验证数据库表 + 结构变更后,关联程序是否报语法错误。 + """ + name: str = args.name + obj_type: str = args.type + + parsed = parse_object_name(name, obj_type) + type_label = get_type_config(obj_type).label + + if parsed.src_uri is None: + print(f"\n ✗ {type_label}没有源代码,不支持 syntax-check 操作") + print(f" functiongroup 等是容器对象,不包含可编辑的源代码文件") + raise InvalidNameError(f"{type_label}没有源代码,不支持 syntax-check 操作") + + print("=" * 60) + print(" SAP ADT 语法检查(远程对象,不上传不激活)") + print("=" * 60) + print(f" 对象类型: {type_label}") + print(f" 对象名称: {parsed.display_name}") + + print(f"\n → 检查对象是否存在...") + if not client.object_exists(parsed.obj_uri): + print(f" ✗ 对象不存在: {parsed.display_name}") + raise ObjectNotFoundError(parsed.display_name, obj_type) + print(" ✓ 对象存在") + + print(f"\n → 正在执行语法检查...") + ok, messages = client.syntax_check(name, parsed.obj_uri) + + errors = [m for m in messages if m["type"] == "E"] + warnings = [m for m in messages if m["type"] == "W"] + + if ok: + print(f"\n ✓ 语法检查通过") + if warnings: + print(f" ({len(warnings)} 个警告)") + for w in warnings: + print(f" [警告] 行 {w['line']}: {w['text']}") + return + + print(f"\n ✗ 语法检查未通过! {len(errors)} 个错误, {len(warnings)} 个警告") + for e in errors: + print(f" [错误] 行 {e['line']}: {e['text']}") + for w in warnings: + print(f" [警告] 行 {w['line']}: {w['text']}") diff --git a/assets/sapcli/commands/upload.py b/assets/sapcli/commands/upload.py new file mode 100644 index 0000000..5f20cdc --- /dev/null +++ b/assets/sapcli/commands/upload.py @@ -0,0 +1,104 @@ +"""upload 命令 — 仅上传源代码到 SAP(锁定→写入→解锁,不检查不激活)。""" +from __future__ import annotations + +import argparse +import logging +import os + +from sapcli.client import ADTClient +from sapcli.commands.crud import _select_transport_request +from sapcli.exceptions import ( + ConfigError, + InvalidNameError, + LockError, + ObjectNotFoundError, +) +from sapcli.types import get_type_config, parse_object_name + +logger = logging.getLogger("sapcli.commands.upload") + + +def cmd_upload(args: argparse.Namespace, client: ADTClient) -> None: + """上传本地源代码到 SAP(仅锁定→写入→解锁,不检查不激活)。 + + 与 sync 的区别: + - 不自动创建对象(对象必须已存在,否则报 ObjectNotFoundError) + - 不执行语法检查 + - 不执行激活 + + 适用于自动化测试场景:修改代码后先安全写入 SAP,避开 activate 的 + 锁定冲突,稍后再单独 activate。 + """ + name: str = args.name + obj_type: str = args.type + file_path: str = args.path + corr_nr: str | None = getattr(args, "corr_nr", None) + + parsed = parse_object_name(name, obj_type) + type_label = get_type_config(obj_type).label + + if parsed.src_uri is None: + print(f"\n ✗ {type_label}没有源代码,不支持 upload 操作") + print(f" functiongroup 等是容器对象,不包含可编辑的源代码文件") + raise InvalidNameError(f"{type_label}没有源代码,不支持 upload 操作") + + if not os.path.isfile(file_path): + print(f"\n ✗ 文件不存在: {file_path}") + raise ConfigError(f"文件不存在: {file_path}") + + with open(file_path, "r", encoding="utf-8") as f: + source = f.read() + + print("=" * 60) + print(" SAP ADT 源代码上传(不检查不激活)") + print("=" * 60) + print(f" 对象类型: {type_label}") + print(f" 对象名称: {parsed.display_name}") + print(f" 本地文件: {file_path}") + print(f" 源代码: {len(source)} 字符, {len(source.splitlines())} 行") + print(f" 流程: 检查 → 锁定 → 写入 → 解锁") + + # 检查对象是否存在(upload 不自动创建,区别于 sync) + print(f"\n → 检查对象是否存在...") + if not client.object_exists(parsed.obj_uri): + print(f" ✗ 对象不存在: {parsed.display_name}") + print(f" upload 不会自动创建对象,请先用 create 或 sync 创建") + raise ObjectNotFoundError(parsed.display_name, obj_type) + print(" ✓ 对象存在") + + # ── 锁定 → 写入 → 解锁(提取自 crud._sync_single,不含检查/激活)── + print("\n ── 锁定 → 写入 → 解锁 ──") + lock_handle: str + if corr_nr: + lock_handle, _ = client.lock(parsed.obj_uri, corr_nr) + print(f" ✓ 锁定成功(传输请求: {corr_nr})") + else: + try: + lock_handle, detected_corr_nr = client.lock(parsed.obj_uri) + if detected_corr_nr: + corr_nr = detected_corr_nr + print(f" ✓ 锁定成功(对象已绑定传输请求: {corr_nr})") + else: + print(f" ✓ 锁定成功(本地对象,无需传输请求)") + except LockError: + print(" ℹ 对象需要传输请求号") + corr_nr = _select_transport_request(client) + if not corr_nr: + print(" ✗ 锁定失败: 无法获取传输请求号") + raise LockError("锁定失败: 无法获取传输请求号", obj_uri=parsed.obj_uri) + lock_handle, _ = client.lock(parsed.obj_uri, corr_nr) + print(f" ✓ 锁定成功(传输请求: {corr_nr})") + + try: + client.set_source(parsed.src_uri, source, lock_handle, corr_nr) + print(f" ✓ 源代码写入成功") + finally: + client.unlock(parsed.obj_uri, lock_handle) + print(f" ✓ 解锁成功") + + print(f"\n {'=' * 60}") + print(f" ✓ {parsed.display_name} 源代码已上传") + print(f" 文件: {file_path}") + print(f" 源代码: {len(source)} 字符, {len(source.splitlines())} 行") + print(f" 注意: 未执行语法检查和激活,请按需单独执行 syntax-check / activate") + print(f" {'=' * 60}")