feat(jira): 增强提交检查和文档路径规范

- 添加已推送提交检查,避免不安全的 amend 操作
- 规范文档保存路径为 `doc/{日期}` 格式
- 扩展 markdown 解析支持表格和更多格式
- 优化 Jira 搜索 API 调用方式
This commit is contained in:
2026-04-02 17:38:39 +08:00
parent 062e9a652b
commit 44b13c96f6
5 changed files with 244 additions and 26 deletions
+50 -11
View File
@@ -29,13 +29,15 @@ description: G3FO 项目 Git 提交规范自动化工具。**AI 必须严格按
1. **禁止 `git push`**。本技能只做到 amend 为止。
2. **禁止跳过** `git rev-parse <hash>`;amend 场景下禁止在 **hash ≠ HEAD** 时仍执行 amend。
3. **禁止用 Atlassian MCP 或其它方式冒充「已上传 MD 附件」**;上传文件 **必须** 使用 `scripts/upload_attachment.py`(MCP 无可靠上传能力时不得虚构成功)。
4. **禁止**在用户未选定 Issue、也未完成新建并取得 Key 的情况下,把某 Key 写进 commit message。
5. **禁止**未读 `references/jira_commit_docs_policy.md`(或本 SKILL 中的条件表)就默认「三份全要」或「一律不要文档」。
3. **禁止 amend 已推送的提交**(除非用户明确要求并确认风险)。使用 `git branch -r --contains <hash>` 检测。
4. **禁止用 Atlassian MCP 或其它方式冒充「已上传 MD 附件」**;上传文件 **必须** 使用 `scripts/upload_attachment.py`(MCP 无可靠上传能力时不得虚构成功)。
5. **禁止**在用户未选定 Issue、也未完成新建并取得 Key 的情况下,把某 Key 写进 commit message。
6. **禁止**未读 `references/jira_commit_docs_policy.md`(或本 SKILL 中的条件表)就默认「三份全要」或「一律不要文档」。
7. **禁止**在用户已提供 Jira 编号时仍执行搜索、展示列表或创建新 Issue。
### 3. Step 3 交互规则(易走偏)
- 用户**已给** Jira 编号 → 直接进入 Step 4,Key = 用户给的编号。
- 用户**已给** Jira 编号 → **直接进入 Step 4**,Key = 用户给的编号,**禁止执行搜索、展示列表或创建新 Issue**。
- 用户**未给** → 搜索展示列表后,**必须等待用户输入 1~N 或 0**(或明确同意新建),**禁止**擅自替用户选一个 Issue 绑定。
- 搜索为空 → 可进入新建流程;新建成功后 Key 以 API 返回为准。
@@ -51,13 +53,16 @@ description: G3FO 项目 Git 提交规范自动化工具。**AI 必须严格按
用户必须提供一个有效的 git 修订号(如 `HEAD` 或具体的 `hash`)。
- **校验逻辑**:使用 `git rev-parse <hash>` 检查修订号是否存在。
- **错误处理**:如果修订号无效或找不到,**必须** 停止操作并提示用户:“找不到修订号 `<hash>`,请提供正确的 git 修订号(例如 HEAD 或 7 位以上的 commit hash)。”
- **错误处理**:如果修订号无效或找不到,**必须** 停止操作并提示用户:"找不到修订号 `<hash>`,请提供正确的 git 修订号(例如 HEAD 或 7 位以上的 commit hash)。"
- **路径要求**:必须在相关的项目根目录下执行 git 命令。如果当前目录不是 git 仓库或不是目标项目,请提示用户切换到正确的项目路径。
- **已 push 提交检测**:使用 `git branch -r --contains <hash>` 检查提交是否已推送到远程。如果已推送,**默认跳过 Step 5(amend)**,仅完成 Jira 绑定和文档上传,并告知用户:
> "提交 `<hash>` 已推送到远程,无法 amend。已完成 Jira 绑定和文档上传。如需修改 commit message,请手动处理或提供特别说明。"
- **例外**:如果用户明确要求修改已 push 的提交(如通过 `--force` 或其他方式),需用户确认风险后再执行。
**2. Jira 编号 (可选)**
用户可以主动提供 Jira 编号(如 `G3SF-123`)。
- 如果提供了 Jira 编号,直接引用该编号,跳过“查找/选择 Jira 任务”的步骤。
- 如果提供了 Jira 编号,**直接使用该编号,跳过「查找/选择/新建 Jira 任务」的步骤**,进入 Step 4 上传文档和评论。
- 如果未提供,则按流程自动查找或提示用户新建。
**3. Jira 访问方式(二选一)**
@@ -89,12 +94,14 @@ description: G3FO 项目 Git 提交规范自动化工具。**AI 必须严格按
```powershell
git show <hash> # 获取 diff + 元数据
git log -1 --format="%B" <hash> # 获取原始 commit message
git branch -r --contains <hash> # 检查是否已推送到远程(有输出则已 push)
```
分析要点:
- 涉及的服务模块/包名
- 新增/修改/删除的方法与接口
- 逻辑改动的核心目的
- **是否已推送**:如果 `git branch -r --contains <hash>` 有输出,说明已推送,Step 5 将跳过 amend
### Step 2 - 判定任务类型并确定需生成的文档
@@ -116,20 +123,27 @@ git log -1 --format="%B" <hash> # 获取原始 commit message
### Step 2(续)- 生成所需报告内容
对上述判定为“需要”的文档,按 `references/workflow.md` 中的模板生成内容(可先存于内存或临时文件):
对上述判定为"需要"的文档,按 `references/workflow.md` 中的模板生成内容(可先存于内存或临时文件):
- **开发计划**:步骤、设计方法、技术决策。
- **影响分析**:改动概览、方法级分析、影响范围、风险与回滚、验证与测试(模板见 workflow.md)。
- **任务摘要**:所完成工作的简要回顾,以及任何偏离计划之处。
生成后先不按“公司附件名”落盘,等取得 Jira 编号后再以 `<JIRA-ID>_*.md` 命名保存并上传。
生成后先不按"公司附件名"落盘,等取得 Jira 编号后再以 `<JIRA-ID>_*.md` 命名保存并上传。
**文档保存路径规则**:
- 所有生成的 MD 文档统一保存到项目根目录下的 `doc/{日期}` 文件夹
- 日期格式为 `YYYY-MM-DD`(如 `doc/2026-03-26`)
- 若该文件夹不存在,**必须先创建**再保存文件
- 示例路径:`doc/2026-03-26/G3SF-123_Impact_Analysis.md`
### Step 3 - 确定 Jira 任务
**前置**:Step 2 已完成(至少已判定文档需求;若需文档,可先草稿内容,**真实 Key 确定后再按 `<KEY>_*.md` 保存**)。
**1. 如果用户已提供 Jira 编号:**
- 直接使用该编号(如 `G3SF-123`),进入 Step 4。
- **直接使用该编号**(如 `G3SF-123`),**跳过查找、选择和新建流程**,直接进入 Step 4。
- 不执行 JQL 搜索,不展示列表,不创建新 Issue。
**2. 如果用户未提供 Jira 编号:**
- 使用 JQL 查找当前用户在 G3SF 项目下的进行中任务(limit 5):
@@ -155,11 +169,15 @@ project = G3SF AND assignee = currentUser() AND statusCategory != Done ORDER BY
**如果选择/新建的任务 ID > 0:**
1. **保存并上传附件**(已配置 `jira_upload.env` 且本任务需要文档时):
将 Step 2 生成的各文档内容,按公司规范命名写入当前项目目录(或 `docs/` 等),再调用上传脚本。PowerShell 中不要用 `&&`,改用 `;` 或换行。
将 Step 2 生成的各文档内容,按公司规范命名写入项目 `doc/{日期}` 目录(日期格式 `YYYY-MM-DD`,若不存在则先创建),再调用上传脚本。PowerShell 中不要用 `&&`,改用 `;` 或换行。
```powershell
# 创建日期目录(若不存在)
$dateFolder = "doc/$(Get-Date -Format 'yyyy-MM-dd')"
if (-not (Test-Path $dateFolder)) { New-Item -ItemType Directory -Path $dateFolder -Force }
# 示例:需要三份文档时,先写入 G3SF-123_Dev_Plan.md、G3SF-123_Impact_Analysis.md、G3SF-123_Task_Summary.md,再:
python skills/g3fo-commit-jira/scripts/upload_attachment.py --config "skills/g3fo-commit-jira/jira_upload.env" --issue "G3SF-123" --file "G3SF-123_Impact_Analysis.md" --file "G3SF-123_Dev_Plan.md" --file "G3SF-123_Task_Summary.md"
python skills/g3fo-commit-jira/scripts/upload_attachment.py --config "skills/g3fo-commit-jira/jira_upload.env" --issue "G3SF-123" --file "$dateFolder/G3SF-123_Impact_Analysis.md" --file "$dateFolder/G3SF-123_Dev_Plan.md" --file "$dateFolder/G3SF-123_Task_Summary.md"
```
若本任务仅需影响分析,则只写入并上传 `G3SF-123_Impact_Analysis.md`。未配置 `jira_upload.env` 时跳过上传;有 MCP 时可仅写评论,无 MCP 则必须配置 env 才能完成评论。
@@ -193,6 +211,17 @@ project = G3SF AND assignee = currentUser() AND statusCategory != Done ORDER BY
### Step 5 - 更新 Git 提交信息
**前置**:Step 4 已按政策完成(无需文档的绑定类任务可跳过上传/评论,但须已有 Key)。
**检查提交是否已推送**:
```powershell
git branch -r --contains <hash>
```
- 如果有输出,说明提交已推送到远程分支,**跳过 amend**,告知用户:
> "提交 `<hash>` 已推送到远程,无法 amend。已完成 Jira 绑定和文档上传。如需修改 commit message,请手动处理或提供特别说明。"
- 如果无输出,说明提交未推送,继续执行 amend。
**执行 amend(仅当未推送时)**:
使用获取到的 Jira 编号,通过 `git commit --amend` 更新 commit message。**严禁 push**。
**注意**:在 Windows PowerShell 环境下,需确保 UTF8 编码以防止中文乱码。
@@ -211,10 +240,20 @@ git commit --amend -m "$origMsg`n`n**** Jira $jiraKey" --cleanup=verbatim
完成后告知用户:
**未推送时**:
```
绑定成功:
Jira: G3SF-123 (https://your-jira/browse/G3SF-123)
Commit: <git log --oneline -1 的结果>
已追加 Jira 编号到 commit message
```
**已推送时**:
```
绑定完成(提交已推送,未修改 commit message):
Jira: G3SF-123 (https://your-jira/browse/G3SF-123)
Commit: <git log --oneline -1 的结果>
提示:提交已推送到远程,如需修改 commit message 请手动处理
```
---
@@ -10,6 +10,7 @@
- [ ] 在 **目标业务仓库根目录** 执行 git(`git rev-parse --is-inside-work-tree` 为真)
- [ ] 已执行 `git rev-parse <hash>`,修订号存在
- [ ] 若要对提交做 amend:已确认 `git rev-parse HEAD` **等于** `git rev-parse <hash>`(否则停止,提示用户)
- [ ] 已检查 `git branch -r --contains <hash>`,确认提交是否已推送(已推送则跳过 amend)
---
@@ -35,15 +36,18 @@
## Step 3 — Jira Key
- [ ] 已有 **真实 Issue Key**(如 `G3SF-123`):来自用户直给、用户从列表选择、或 `create` 成功返回
- [ ] 若用户已提供 Jira 编号:**跳过搜索、展示列表和新建流程**,直接使用该编号
- [ ] 若走列表:已 **等用户选 1~N 或 0**,未擅自替用户绑定
**未取得真实 Key 前:禁止** 使用 `<JIRA-ID>_*.md` 落盘上传(禁止占位符 Key)。
**用户已提供 Jira 时:禁止** 执行搜索、展示列表或创建新 Issue。
---
## Step 4 — 写入 Jira
- [ ] 需要附件时:文件名为 `<KEY>_Dev_Plan.md` / `_Impact_Analysis.md` / `_Task_Summary.md`(仅实际上传需要的)
- [ ] 文档保存路径:`doc/{日期}`(日期格式 `YYYY-MM-DD`),若目录不存在则先创建
- [ ] 附件 **仅** 通过 `scripts/upload_attachment.py` + `jira_upload.env`(不用 MCP 冒充上传)
- [ ] 需要影响分析进评论时:MCP `jira_add_comment` 或 `jira_cli.py comment`(与政策一致)
- [ ] 新建 Issue 时:Summary 带 `[G3SF]`,类型 Task,描述/评论与内容一致
@@ -52,7 +56,8 @@
## Step 5 — Git amend
- [ ] 仅 `git commit --amend`,**未** `git push`
- [ ] 已检查 `git branch -r --contains <hash>`:若已推送,**跳过 amend**,告知用户
- [ ] 若未推送:仅 `git commit --amend`,**未** `git push`
- [ ] Windows 已按 SKILL 处理 UTF-8 / `--cleanup=verbatim`(如适用)
- [ ] 若 message 已含 `**** Jira`,未重复追加
@@ -67,6 +72,8 @@
| 用 MCP 上传 md | 改用 `upload_attachment.py` |
| 简单拼写改却写三份文档 | 重读政策表,按类型裁剪 |
| 执行了 push | 违反技能范围,后续勿再 push |
| amend 已推送的提交 | 先检查 `git branch -r --contains`,已推送则跳过 amend |
| 用户已给 Jira 却还搜索/新建 | 直接使用用户提供的编号,跳过 Step 3 的搜索流程 |
---
@@ -46,3 +46,9 @@
示例:工单 `G3SF-123` 的三个附件命名为
`G3SF-123_Dev_Plan.md`、`G3SF-123_Impact_Analysis.md`、`G3SF-123_Task_Summary.md`。
**本地保存路径**:
- 所有文档统一保存到项目根目录下的 `doc/{日期}` 文件夹
- 日期格式为 `YYYY-MM-DD`(如 `doc/2026-03-26`)
- 若该文件夹不存在,**必须先创建**再保存文件
- 完整路径示例:`doc/2026-03-26/G3SF-123_Impact_Analysis.md`
+53 -2
View File
@@ -42,6 +42,10 @@ $head = git rev-parse HEAD
$provided = git rev-parse <hash>
if ($head -ne $provided) { Write-Error "提交不是 HEAD,无法 amend" }
# 检查提交是否已推送到远程
git branch -r --contains <hash>
# 如果有输出,说明已推送,Step 5 将跳过 amend
# 获取完整 diff 和元数据
git show <hash>
@@ -63,6 +67,7 @@ git diff-tree --no-commit-id -r --name-status <hash>
| `+` 行 | 新增代码(方法签名、接口、配置) |
| `-` 行 | 删除/替换代码 |
| 文件路径中的包名 | 推断所属服务(`g3fo-trade-service`, `g3fo-margin-service` 等) |
| `git branch -r --contains` 输出 | 是否已推送(有输出 = 已推送,跳过 amend) |
G3FO 服务包路径规律:
- `com.afe.g3fo.<service-short-name>.*` → 对应服务
@@ -171,6 +176,17 @@ G3FO 服务包路径规律:
## Step 3: Jira 任务查找与确认
### 用户已提供 Jira 编号
如果用户已提供 Jira 编号(如 `G3SF-123`):
- **直接使用该编号**,跳过 JQL 搜索、列表展示和新建流程
- **禁止**执行搜索、展示列表或创建新 Issue
- 直接进入 Step 4 上传文档和评论
### 用户未提供 Jira 编号
执行 JQL 查询:
### JQL 查询
```
@@ -231,12 +247,23 @@ Summary 生成规则:
- `<JIRA-ID>_Dev_Plan.md`
- `<JIRA-ID>_Impact_Analysis.md`
- `<JIRA-ID>_Task_Summary.md`
仅保存本任务类型要求的那几份(见 [2.1 按任务类型确定需生成的文档](#21-按任务类型确定需生成的文档))。保存位置:当前 Git 项目根目录或项目约定目录(如 `docs/`)。
仅保存本任务类型要求的那几份(见 [2.1 按任务类型确定需生成的文档](#21-按任务类型确定需生成的文档))。
**保存路径规则**:
- 所有文档统一保存到项目根目录下的 `doc/{日期}` 文件夹
- 日期格式为 `YYYY-MM-DD`(如 `doc/2026-03-26`)
- 若该文件夹不存在,**必须先创建**再保存文件
- 完整路径示例:`doc/2026-03-26/G3SF-123_Impact_Analysis.md`
2. **上传到 Jira 附件**:Atlassian MCP 不支持上传文件,使用本技能自带脚本上传上述 MD 到当前 Issue:
- 配置文件:在技能目录下配置 `jira_upload.env`(`JIRA_BASE_URL`、`JIRA_EMAIL`、`JIRA_API_TOKEN`),详见 `references/env_config.md`。
- 命令示例(PowerShell 用 `;` 连接,勿用 `&&`),按需上传多文件:
```powershell
python skills/g3fo-commit-jira/scripts/upload_attachment.py --config "skills/g3fo-commit-jira/jira_upload.env" --issue "G3SF-101" --file "G3SF-101_Impact_Analysis.md" --file "G3SF-101_Dev_Plan.md" --file "G3SF-101_Task_Summary.md"
# 创建日期目录(若不存在)
$dateFolder = "doc/$(Get-Date -Format 'yyyy-MM-dd')"
if (-not (Test-Path $dateFolder)) { New-Item -ItemType Directory -Path $dateFolder -Force }
python skills/g3fo-commit-jira/scripts/upload_attachment.py --config "skills/g3fo-commit-jira/jira_upload.env" --issue "G3SF-101" --file "$dateFolder/G3SF-101_Impact_Analysis.md" --file "$dateFolder/G3SF-101_Dev_Plan.md" --file "$dateFolder/G3SF-101_Task_Summary.md"
```
- 未配置 `jira_upload.env` 时跳过上传,仅写评论。
@@ -259,6 +286,28 @@ Summary 生成规则:
## Step 5: Git Amend 命令详解
### 检查提交是否已推送
在执行 amend 前,必须检查提交是否已推送到远程:
```powershell
# 检查提交是否已推送
$pushedBranches = git branch -r --contains <hash>
if ($pushedBranches) {
Write-Host "提交已推送到远程,跳过 amend"
Write-Host "已完成 Jira 绑定和文档上传"
Write-Host "如需修改 commit message,请手动处理或提供特别说明"
# 跳过 amend,直接结束
return
}
```
**已推送的处理**:
- 如果 `git branch -r --contains <hash>` 有输出,说明提交已推送到远程分支
- **跳过 amend**,仅完成 Jira 绑定和文档上传
- 告知用户提交已推送,无法 amend
- 例外:用户明确要求修改已推送的提交时,需确认风险后再执行
### PowerShell(Windows 环境)
在 Windows PowerShell 中,直接使用 `$origMsg` 可能会导致编码问题(乱码)。必须显式指定输出编码为 UTF8。
@@ -331,6 +380,7 @@ git log --oneline -1 # 查看 commit 摘要
|---------|---------|---------|
| 修订号不存在 | `git rev-parse <hash>` 报错 | **停止**,提示用户提供正确的修订号。 |
| 提交 hash 不是 HEAD | `git rev-parse HEAD` ≠ `git rev-parse <hash>` | 停止,提示用户确认 hash(只有 HEAD 才能 amend)。 |
| 提交已推送到远程 | `git branch -r --contains <hash>` 有输出 | **跳过 amend**,仅完成 Jira 绑定和文档上传,告知用户提交已推送。 |
| 项目路径不正确 | `git rev-parse --is-inside-work-tree` 失败 | 提示用户在正确的 git 项目目录下操作。 |
| 无 MCP 且未配置 jira_upload.env | 无法执行 jira_cli / 上传 | **必须**配置 `jira_upload.env` 或改用 Cursor + MCP。 |
| Atlassian MCP 未鉴权 | `STATUS.md` 有提示 / 工具调用返回 401 | 调用 `mcp_auth`,或改用 `jira_cli.py`。 |
@@ -338,6 +388,7 @@ git log --oneline -1 # 查看 commit 摘要
| Jira 项目 G3SF 不存在 | 创建/搜索返回 404/400 | 提示用户确认 `JIRA_BASE_URL`、项目 key;`create` 可用 `--project` |
| 工作区有未提交变更 | `git status --porcelain` 有输出 | 警告:amend 会将未暂存变更排除在外,建议先 `git add` 或 stash |
| commit message 中已有 Jira 行 | message 中包含 `**** Jira` | 跳过追加,提示用户该提交已绑定 |
| 用户已提供 Jira 编号 | 用户输入包含 Jira key | **跳过搜索和新建**,直接使用用户提供的编号 |
| Jira 搜索无结果 | 工具返回空列表 | 直接进入新建流程,无需用户确认 |
| 未配置 jira_upload.env | 上传前检查配置不存在 | **有 MCP**:跳过上传,仅评论。**无 MCP**:无法完成 Jira 操作,须配置 env 或使用 MCP。 |
| 任务类型判定为「无需文档」 | 按政策表属简单漏洞/琐碎改动 | 不生成报告、不写评论、不上传附件;仍执行 Jira 绑定与 commit --amend,将 Jira 编号追加到 commit message |
+127 -12
View File
@@ -57,14 +57,18 @@ def _text_nodes(s: str) -> List[Dict[str, Any]]:
def parse_inline_adf(s: str) -> List[Dict[str, Any]]:
"""
Parse inline **bold**, `code`, and [text](url) into ADF text nodes with marks.
Parse inline **bold**, ~~strikethrough~~, `code`, *italic*, [text](url), ***bold italic***
into ADF text nodes with marks.
"""
if not s:
return []
# Order: links, bold, code (non-greedy). Avoid ``` by not matching double backtick start.
# Order: links, bold+italic, bold, strikethrough, italic, code (non-greedy).
pattern = re.compile(
r"\[([^\]]+)\]\(([^)]+)\)" # [label](url)
r"|\*\*\*(.+?)\*\*\*" # ***bold italic***
r"|\*\*(.+?)\*\*" # **bold**
r"|~~(.+?)~~" # ~~strikethrough~~
r"|(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)" # *italic* (avoid matching **)
r"|(?<!`)`([^`\n]+)`(?!`)" # `code`
)
nodes: List[Dict[str, Any]] = []
@@ -84,11 +88,27 @@ def parse_inline_adf(s: str) -> List[Dict[str, Any]]:
)
elif m.group(3) is not None:
nodes.append(
{"type": "text", "text": m.group(3), "marks": [{"type": "strong"}]}
{
"type": "text",
"text": m.group(3),
"marks": [{"type": "strong"}, {"type": "em"}],
}
)
elif m.group(4) is not None:
nodes.append(
{"type": "text", "text": m.group(4), "marks": [{"type": "code"}]}
{"type": "text", "text": m.group(4), "marks": [{"type": "strong"}]}
)
elif m.group(5) is not None:
nodes.append(
{"type": "text", "text": m.group(5), "marks": [{"type": "strike"}]}
)
elif m.group(6) is not None:
nodes.append(
{"type": "text", "text": m.group(6), "marks": [{"type": "em"}]}
)
elif m.group(7) is not None:
nodes.append(
{"type": "text", "text": m.group(7), "marks": [{"type": "code"}]}
)
last = m.end()
if last < len(s):
@@ -98,6 +118,78 @@ def parse_inline_adf(s: str) -> List[Dict[str, Any]]:
return nodes
def _split_table_row(line: str) -> List[str]:
"""Split a pipe-delimited table row into cells, stripping whitespace."""
parts = line.split("|")
cells = []
for p in parts:
c = p.strip()
if c:
cells.append(c)
return cells
def _is_separator_row(cells: List[str]) -> bool:
"""Check if a row is a GFM table separator (e.g., |---|---|)."""
return bool(cells) and all(re.fullmatch(r"-{3,}", c.strip()) for c in cells)
def _is_table_row(line: str) -> bool:
"""Detect if a line looks like a GFM table row."""
stripped = line.strip()
if not stripped.startswith("|"):
return False
cells = _split_table_row(stripped)
return len(cells) >= 1
def _parse_table(lines: List[str], start: int, n: int):
"""
Parse a GFM table starting at index start.
Returns (adf_table_dict, next_index).
"""
header_cells = _split_table_row(lines[start].strip())
num_cols = len(header_cells)
i = start + 1
# Skip separator row if present
if i < n and _is_separator_row(_split_table_row(lines[i].strip())):
i += 1
data_rows: List[List[str]] = []
while i < n:
s = lines[i].strip()
if not s or not _is_table_row(s):
break
row_cells = _split_table_row(s)
# Pad or trim to match header column count
while len(row_cells) < num_cols:
row_cells.append("")
data_rows.append(row_cells[:num_cols])
i += 1
# Build ADF table
def _make_cell(text: str, is_header: bool = False) -> Dict[str, Any]:
cell_type = "tableHeader" if is_header else "tableCell"
inline_nodes = parse_inline_adf(text) or _text_nodes(text or " ")
return {
"type": cell_type,
"content": [{"type": "paragraph", "content": inline_nodes}],
}
def _make_row(cell_texts: List[str], is_header: bool = False) -> Dict[str, Any]:
return {
"type": "tableRow",
"content": [_make_cell(c, is_header=is_header) for c in cell_texts],
}
table_content: List[Dict[str, Any]] = [_make_row(header_cells, is_header=True)]
for dr in data_rows:
table_content.append(_make_row(dr))
return {"type": "table", "content": table_content}, i
def _paragraph_from_buffer(lines: List[str]) -> Optional[Dict[str, Any]]:
if not lines:
return None
@@ -164,6 +256,13 @@ def markdown_to_adf(text: str) -> Dict[str, Any]:
i += 1
continue
# GFM Table: | col1 | col2 |
if _is_table_row(stripped):
flush_paragraph()
table_adf, i = _parse_table(lines, i, n)
content.append(table_adf)
continue
# Fenced code block
if stripped.startswith("```"):
flush_paragraph()
@@ -293,6 +392,22 @@ def plain_to_adf(text: str) -> Dict[str, Any]:
return markdown_to_adf(text)
def unescape_text(text: str) -> str:
"""
Unescape common escape sequences that may appear as literal strings
in CLI arguments or generated content.
Converts \\n -> newline, \\t -> tab, \\\\ -> backslash, etc.
"""
if not text:
return text
result = text.replace("\\\\", "\x00ESCAPED_BACKSLASH\x00")
result = result.replace("\\n", "\n")
result = result.replace("\\t", "\t")
result = result.replace("\\r", "\r")
result = result.replace("\x00ESCAPED_BACKSLASH\x00", "\\")
return result
def _out(data: Any, fmt: str) -> None:
if fmt == "json":
print(json.dumps(data, ensure_ascii=False, indent=2))
@@ -318,16 +433,16 @@ def cmd_myself(base: str, email: str, token: str, fmt: str) -> int:
def cmd_search(base: str, email: str, token: str, jql: str, limit: int, fmt: str) -> int:
url = f"{base}/rest/api/3/search"
params = {
url = f"{base}/rest/api/3/search/jql"
payload = {
"jql": jql,
"maxResults": limit,
"fields": "key,summary,status,assignee,updated",
"fields": ["key", "summary", "status", "assignee", "updated"],
}
r = requests.get(
r = requests.post(
url,
headers=session_headers(email, token, json_body=False),
params=params,
headers=session_headers(email, token),
json=payload,
timeout=60,
)
if r.status_code != 200:
@@ -455,8 +570,8 @@ def read_body(description: Optional[str], path: Optional[str]) -> str:
p = Path(path)
if not p.is_file():
raise FileNotFoundError(f"File not found: {p}")
return p.read_text(encoding="utf-8")
return description or ""
return unescape_text(p.read_text(encoding="utf-8"))
return unescape_text(description or "")
def main() -> int: