mirror of
https://github.com/cita-777/metapi.git
synced 2026-10-31 19:47:31 +08:00
680 lines
24 KiB
Python
Executable File
680 lines
24 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
向 journal 文件追加一次会话,并更新 index.md。
|
||
|
||
用法:
|
||
python3 add_session.py --title "标题" --commit "hash" --summary "摘要" [--package cli]
|
||
python3 add_session.py --title "标题" --branch "feat/my-branch"
|
||
|
||
# 通过 stdin 传入详细内容(必须显式使用 --stdin):
|
||
cat << 'EOF' | python3 add_session.py --stdin --title "标题" --summary "摘要"
|
||
<会话内容>
|
||
EOF
|
||
|
||
# 结构化内容(可重复;没有条目的 section 会省略):
|
||
python3 add_session.py --title "标题" --change "完成某项变更" --test "运行某项测试" --next-step "继续某项工作"
|
||
|
||
分支解析顺序:
|
||
1. --branch CLI 参数(显式值)
|
||
2. 当前任务的 task.json branch 字段(仍存在时)
|
||
3. git branch --show-current(自动探测)
|
||
4. None(安全省略)
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import re
|
||
import sys
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
|
||
from common.paths import (
|
||
DIR_TASKS,
|
||
DIR_WORKFLOW,
|
||
FILE_JOURNAL_PREFIX,
|
||
get_repo_root,
|
||
get_current_task,
|
||
get_developer,
|
||
get_workspace_dir,
|
||
)
|
||
from common.developer import ensure_developer
|
||
from common.git import run_git
|
||
from common.log import Colors, colored
|
||
from common.safe_commit import (
|
||
print_gitignore_warning,
|
||
safe_git_add,
|
||
safe_trellis_paths_to_add,
|
||
)
|
||
from common.tasks import load_task
|
||
from common.types import TaskInfo
|
||
from common.config import (
|
||
get_packages,
|
||
get_session_auto_commit,
|
||
get_session_commit_message,
|
||
get_max_journal_lines,
|
||
is_monorepo,
|
||
resolve_package,
|
||
validate_package,
|
||
)
|
||
|
||
|
||
# =============================================================================
|
||
# 辅助函数
|
||
# =============================================================================
|
||
|
||
def get_latest_journal_info(dev_dir: Path) -> tuple[Path | None, int, int]:
|
||
"""获取最新 journal 文件信息。
|
||
|
||
返回:
|
||
`(file_path, file_number, line_count)` 元组。
|
||
"""
|
||
latest_file: Path | None = None
|
||
latest_num = -1
|
||
|
||
for f in dev_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md"):
|
||
if not f.is_file():
|
||
continue
|
||
|
||
match = re.search(r"(\d+)$", f.stem)
|
||
if match:
|
||
num = int(match.group(1))
|
||
if num > latest_num:
|
||
latest_num = num
|
||
latest_file = f
|
||
|
||
if latest_file:
|
||
lines = len(latest_file.read_text(encoding="utf-8").splitlines())
|
||
return latest_file, latest_num, lines
|
||
|
||
return None, 0, 0
|
||
|
||
|
||
def get_current_session(index_file: Path) -> int:
|
||
"""从 index.md 获取当前 session 编号。"""
|
||
if not index_file.is_file():
|
||
return 0
|
||
|
||
content = index_file.read_text(encoding="utf-8")
|
||
for line in content.splitlines():
|
||
if "Total Sessions" in line or "会话总数" in line:
|
||
match = re.search(r":\s*(\d+)", line)
|
||
if match:
|
||
return int(match.group(1))
|
||
return 0
|
||
|
||
|
||
def _extract_journal_num(filename: str) -> int:
|
||
"""从文件名提取 journal 编号,用于排序。"""
|
||
match = re.search(r"(\d+)", filename)
|
||
return int(match.group(1)) if match else 0
|
||
|
||
|
||
def count_journal_files(dev_dir: Path, active_num: int) -> str:
|
||
"""统计 journal 文件并返回表格行。"""
|
||
active_file = f"{FILE_JOURNAL_PREFIX}{active_num}.md"
|
||
result_lines = []
|
||
|
||
files = sorted(
|
||
[f for f in dev_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md") if f.is_file()],
|
||
key=lambda f: _extract_journal_num(f.stem),
|
||
reverse=True
|
||
)
|
||
|
||
for f in files:
|
||
filename = f.name
|
||
lines = len(f.read_text(encoding="utf-8").splitlines())
|
||
status = "活动" if filename == active_file else "已归档"
|
||
result_lines.append(f"| `{filename}` | ~{lines} | {status} |")
|
||
|
||
return "\n".join(result_lines)
|
||
|
||
|
||
def get_current_git_branch(repo_root: Path) -> str | None:
|
||
"""返回当前检出的 branch;detached 或非 git 状态返回 None。"""
|
||
rc, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
|
||
if rc != 0:
|
||
return None
|
||
detected = branch_out.strip()
|
||
return detected or None
|
||
|
||
|
||
def branch_ref_exists(repo_root: Path, branch: str) -> bool:
|
||
"""branch 在本地或本地 origin ref 中存在时返回 True。"""
|
||
for ref in (f"refs/heads/{branch}", f"refs/remotes/origin/{branch}"):
|
||
rc, _, _ = run_git(["show-ref", "--verify", "--quiet", ref], cwd=repo_root)
|
||
if rc == 0:
|
||
return True
|
||
return False
|
||
|
||
|
||
def resolve_session_branch(
|
||
repo_root: Path,
|
||
cli_branch: str | None,
|
||
task_data: TaskInfo | None,
|
||
) -> str | None:
|
||
"""解析 journal branch,不盲信过期的 task.json branch 字段。"""
|
||
if cli_branch:
|
||
return cli_branch
|
||
|
||
current_branch = get_current_git_branch(repo_root)
|
||
raw_task_branch = task_data.raw.get("branch") if task_data else None
|
||
task_branch = raw_task_branch.strip() if isinstance(raw_task_branch, str) else ""
|
||
if not task_branch:
|
||
return current_branch
|
||
|
||
if branch_ref_exists(repo_root, task_branch):
|
||
return task_branch
|
||
|
||
if current_branch:
|
||
print(
|
||
f"警告:task.json 中的 branch '{task_branch}' 在本地和 origin/{task_branch} 均不存在;改用当前分支 '{current_branch}'。",
|
||
file=sys.stderr,
|
||
)
|
||
return current_branch
|
||
|
||
print(
|
||
f"警告:task.json 中的 branch '{task_branch}' 在本地和 origin/{task_branch} 均不存在;本次日志省略分支。",
|
||
file=sys.stderr,
|
||
)
|
||
return None
|
||
|
||
|
||
def is_git_worktree(repo_root: Path) -> bool:
|
||
"""repo_root 是链接 worktree(而非主工作树)时返回 True。
|
||
|
||
标准判断方式:将 `git rev-parse --git-dir`(每个 worktree 独立)和
|
||
`git rev-parse --git-common-dir`(所有 worktree 共享)解析为绝对路径后比较;
|
||
在主工作树中两者相同。
|
||
"""
|
||
rc_dir, git_dir, _ = run_git(["rev-parse", "--git-dir"], cwd=repo_root)
|
||
rc_common, git_common_dir, _ = run_git(
|
||
["rev-parse", "--git-common-dir"], cwd=repo_root
|
||
)
|
||
if rc_dir != 0 or rc_common != 0:
|
||
return False
|
||
|
||
git_dir_path = (repo_root / git_dir.strip()).resolve()
|
||
git_common_dir_path = (repo_root / git_common_dir.strip()).resolve()
|
||
return git_dir_path != git_common_dir_path
|
||
|
||
|
||
def warn_if_parallel_worktree(repo_root: Path) -> None:
|
||
"""输出非阻塞提示:并行 worktree/branch 的 index.md 冲突是预期且安全的。
|
||
|
||
仅在启用 `session_auto_commit` 的链接 git worktree(不是主工作树)中输出
|
||
(#415 quick-fix tier)。
|
||
"""
|
||
if not get_session_auto_commit(repo_root):
|
||
return
|
||
if not is_git_worktree(repo_root):
|
||
return
|
||
print(
|
||
colored(
|
||
"[提示] 当前在启用 session_auto_commit 的 git worktree 中运行:"
|
||
"journal-*.md 会通过 .gitattributes 自动合并;并行 worktree/branch 的 "
|
||
"index.md 冲突属于预期情况,可任选一侧解决(任务状态在 task.json,"
|
||
"不在 index.md)。详见 .trellis/spec/backend/directory-structure.md。",
|
||
Colors.YELLOW,
|
||
),
|
||
file=sys.stderr,
|
||
)
|
||
|
||
|
||
def create_new_journal_file(
|
||
dev_dir: Path, num: int, developer: str, today: str, max_lines: int = 2000,
|
||
) -> Path:
|
||
"""创建新的 journal 文件。"""
|
||
prev_num = num - 1
|
||
new_file = dev_dir / f"{FILE_JOURNAL_PREFIX}{num}.md"
|
||
|
||
content = f"""# 开发日志 - {developer}(第 {num} 部分)
|
||
|
||
> 接续 `{FILE_JOURNAL_PREFIX}{prev_num}.md`(该文件在约 {max_lines} 行时归档)
|
||
> 开始日期:{today}
|
||
|
||
---
|
||
|
||
"""
|
||
new_file.write_text(content, encoding="utf-8")
|
||
return new_file
|
||
|
||
|
||
def _render_bullet_section(header: str, items: list[str], bullet_prefix: str = "- ") -> str:
|
||
"""将 Markdown section 渲染为项目符号;无内容时返回 ""。
|
||
|
||
没有值的 section 会从渲染结果中完全省略,不会回退到占位字符串。
|
||
"""
|
||
if not items:
|
||
return ""
|
||
bullets = "\n".join(f"{bullet_prefix}{item}" for item in items)
|
||
return f"\n\n### {header}\n\n{bullets}"
|
||
|
||
|
||
def _render_main_changes(changes: list[str], extra_content: str | None) -> str:
|
||
"""根据 --change 项目或自由文本渲染“主要变更”section。"""
|
||
if changes:
|
||
return _render_bullet_section("主要变更", changes)
|
||
if extra_content:
|
||
return f"\n\n### 主要变更\n\n{extra_content}"
|
||
return ""
|
||
|
||
|
||
def generate_session_content(
|
||
session_num: int,
|
||
title: str,
|
||
commit: str,
|
||
summary: str,
|
||
today: str,
|
||
package: str | None = None,
|
||
branch: str | None = None,
|
||
changes: list[str] | None = None,
|
||
extra_content: str | None = None,
|
||
tests: list[str] | None = None,
|
||
next_steps: list[str] | None = None,
|
||
) -> str:
|
||
"""生成 session 内容。"""
|
||
if commit and commit != "-":
|
||
commit_table = """| 哈希 | 提交说明 |
|
||
|------|---------|"""
|
||
for c in commit.split(","):
|
||
c = c.strip()
|
||
commit_table += f"\n| `{c}` |(参见 git log)|"
|
||
else:
|
||
commit_table = "(无提交 - 规划会话)"
|
||
|
||
package_line = f"\n**包**:{package}" if package else ""
|
||
branch_line = f"\n**分支**:`{branch}`" if branch else ""
|
||
|
||
main_changes_section = _render_main_changes(changes or [], extra_content)
|
||
testing_section = _render_bullet_section("测试", tests or [], bullet_prefix="- [OK] ")
|
||
next_steps_section = _render_bullet_section("后续步骤", next_steps or [])
|
||
|
||
return f"""
|
||
|
||
## 会话 {session_num}:{title}
|
||
|
||
**日期**:{today}
|
||
**任务**:{title}{package_line}{branch_line}
|
||
|
||
### 摘要
|
||
|
||
{summary}{main_changes_section}
|
||
|
||
### Git 提交
|
||
|
||
{commit_table}{testing_section}
|
||
|
||
### 状态
|
||
|
||
[OK] **已完成**{next_steps_section}
|
||
"""
|
||
|
||
|
||
def update_index(
|
||
index_file: Path,
|
||
dev_dir: Path,
|
||
title: str,
|
||
commit: str,
|
||
new_session: int,
|
||
active_file: str,
|
||
today: str,
|
||
branch: str | None = None,
|
||
) -> bool:
|
||
"""用新的 session 信息更新 index.md。"""
|
||
# 格式化 commit 以便展示。
|
||
commit_display = "-"
|
||
if commit and commit != "-":
|
||
commit_display = re.sub(r"([a-f0-9]{7,})", r"`\1`", commit.replace(",", ", "))
|
||
|
||
# 从 active_file 名称获取文件编号。
|
||
match = re.search(r"(\d+)", active_file)
|
||
active_num = int(match.group(1)) if match else 0
|
||
files_table = count_journal_files(dev_dir, active_num)
|
||
|
||
print(f"正在更新 index.md(会话 {new_session})……")
|
||
print(f" 标题:{title}")
|
||
print(f" 提交:{commit_display}")
|
||
print(f" 活动文件:{active_file}")
|
||
print()
|
||
|
||
content = index_file.read_text(encoding="utf-8")
|
||
|
||
if "@@@auto:current-status" not in content:
|
||
print("错误:index.md 中找不到受管 marker,请确认 marker 未被删除。", file=sys.stderr)
|
||
return False
|
||
|
||
# 处理各个 section。
|
||
lines = content.splitlines()
|
||
new_lines = []
|
||
|
||
in_current_status = False
|
||
in_active_documents = False
|
||
in_session_history = False
|
||
header_written = False
|
||
|
||
for line in lines:
|
||
if "@@@auto:current-status" in line:
|
||
new_lines.append(line)
|
||
in_current_status = True
|
||
new_lines.append(f"- **活动文件**:`{active_file}`")
|
||
new_lines.append(f"- **会话总数**:{new_session}")
|
||
new_lines.append(f"- **最近活动**:{today}")
|
||
continue
|
||
|
||
if "@@@/auto:current-status" in line:
|
||
in_current_status = False
|
||
new_lines.append(line)
|
||
continue
|
||
|
||
if "@@@auto:active-documents" in line:
|
||
new_lines.append(line)
|
||
in_active_documents = True
|
||
new_lines.append("| 文件 | 行数 | 状态 |")
|
||
new_lines.append("|------|-------|--------|")
|
||
new_lines.append(files_table)
|
||
continue
|
||
|
||
if "@@@/auto:active-documents" in line:
|
||
in_active_documents = False
|
||
new_lines.append(line)
|
||
continue
|
||
|
||
if "@@@auto:session-history" in line:
|
||
new_lines.append(line)
|
||
in_session_history = True
|
||
header_written = False
|
||
continue
|
||
|
||
if "@@@/auto:session-history" in line:
|
||
in_session_history = False
|
||
new_lines.append(line)
|
||
continue
|
||
|
||
if in_current_status:
|
||
continue
|
||
|
||
if in_active_documents:
|
||
continue
|
||
|
||
if in_session_history:
|
||
# 将旧的 4/6 列表头迁移为仅含 Branch 的 5 列历史表。
|
||
if re.match(
|
||
r"^\|\s*#\s*\|\s*Date\s*\|\s*Title\s*\|\s*Commits\s*\|\s*Branch\s*\|\s*Base Branch\s*\|\s*$",
|
||
line,
|
||
):
|
||
new_lines.append("| # | Date | Title | Commits | Branch |")
|
||
continue
|
||
if re.match(r"^\|\s*#\s*\|\s*Date\s*\|\s*Title\s*\|\s*Commits\s*\|\s*Branch\s*\|\s*$", line):
|
||
new_lines.append("| # | Date | Title | Commits | Branch |")
|
||
continue
|
||
if re.match(r"^\|\s*#\s*\|\s*Date\s*\|\s*Title\s*\|\s*Commits\s*\|\s*$", line):
|
||
new_lines.append("| # | Date | Title | Commits | Branch |")
|
||
continue
|
||
if re.match(
|
||
r"^\|\s*#\s*\|\s*日期\s*\|\s*标题\s*\|\s*提交\s*\|\s*分支\s*\|\s*$",
|
||
line,
|
||
):
|
||
new_lines.append("| # | 日期 | 标题 | 提交 | 分支 |")
|
||
continue
|
||
if re.match(r"^\|[-| ]+\|\s*$", line) and not header_written:
|
||
new_lines.append("|---|------|-------|---------|--------|")
|
||
new_lines.append(f"| {new_session} | {today} | {title} | {commit_display} | `{branch or '-'}` |")
|
||
header_written = True
|
||
continue
|
||
new_lines.append(line)
|
||
continue
|
||
|
||
new_lines.append(line)
|
||
|
||
index_file.write_text("\n".join(new_lines), encoding="utf-8")
|
||
print("[OK] index.md 更新成功!")
|
||
return True
|
||
|
||
|
||
# =============================================================================
|
||
# 主函数
|
||
# =============================================================================
|
||
|
||
def _auto_commit_workspace(repo_root: Path) -> None:
|
||
"""stage 并提交 Trellis 所有的 workspace 和当前任务路径。
|
||
|
||
路径范围严格限制为:当前开发者的 journal 文件和 index.md,以及仅当前任务目录
|
||
(通过 ``get_current_task`` 解析)。绝不对整个 `.trellis/` 执行 `git add`,也不遍历
|
||
所有活动任务目录(#303:并行窗口中的脏任务目录不能被打包进 session 自动提交)。
|
||
如果 `.gitignore` 阻止了具体路径,则警告并跳过,绝不使用 ``-f`` 重试。
|
||
|
||
遵循 ``.trellis/config.yaml`` 中的 ``session_auto_commit``:设置为 ``false`` 时立即
|
||
返回,不触碰 git(journal/index 文件仍由调用方写入磁盘)。
|
||
"""
|
||
if not get_session_auto_commit(repo_root):
|
||
print(
|
||
"[OK] session_auto_commit: false — 跳过 Git stage/commit。",
|
||
file=sys.stderr,
|
||
)
|
||
return
|
||
|
||
commit_msg = get_session_commit_message(repo_root)
|
||
# 解析当前任务,使 stage 范围仅限该目录。ref 是 ``.trellis/tasks/<name>``(或
|
||
# archive/ 下的路径),这里传入不带前缀的名称。
|
||
current = get_current_task(repo_root)
|
||
if current:
|
||
task_name = Path(current).name
|
||
paths = safe_trellis_paths_to_add(repo_root, task_name=task_name)
|
||
else:
|
||
# 当前任务未知(0 个或至少 2 个并行 session,正是 #303 讨论的并行窗口情况)。
|
||
# 不要回退到宽泛的 `tasks_dir.iterdir()` 扫描,否则其他任务的脏目录会再次泄漏
|
||
# 到 session commit。这里只 stage 开发者的 journal/index,跳过所有任务目录。
|
||
paths = [
|
||
p
|
||
for p in safe_trellis_paths_to_add(repo_root, task_name=None)
|
||
if not p.startswith(f"{DIR_WORKFLOW}/{DIR_TASKS}/")
|
||
]
|
||
if not paths:
|
||
print("[OK] 没有需要提交的 workspace 变更。", file=sys.stderr)
|
||
return
|
||
|
||
success, _, err = safe_git_add(paths, repo_root)
|
||
if not success:
|
||
if err and "ignored by" in err.lower():
|
||
print_gitignore_warning(paths)
|
||
else:
|
||
print(
|
||
f"[WARN] git add 失败:{err.strip() if err else '未知错误'}",
|
||
file=sys.stderr,
|
||
)
|
||
return
|
||
|
||
# 检查刚才 stage 的路径是否真的有 staged 变更。
|
||
rc, _, _ = run_git(
|
||
["diff", "--cached", "--quiet", "--", *paths], cwd=repo_root
|
||
)
|
||
if rc == 0:
|
||
print("[OK] 没有需要提交的 workspace 变更。", file=sys.stderr)
|
||
return
|
||
|
||
rc, _, commit_err = run_git(["commit", "-m", commit_msg], cwd=repo_root)
|
||
if rc == 0:
|
||
print(f"[OK] 已自动提交:{commit_msg}", file=sys.stderr)
|
||
else:
|
||
print(
|
||
f"[WARN] 自动提交失败:{commit_err.strip()}",
|
||
file=sys.stderr,
|
||
)
|
||
|
||
|
||
def add_session(
|
||
title: str,
|
||
commit: str = "-",
|
||
summary: str = "未提供会话摘要。",
|
||
changes: list[str] | None = None,
|
||
extra_content: str | None = None,
|
||
tests: list[str] | None = None,
|
||
next_steps: list[str] | None = None,
|
||
auto_commit: bool = True,
|
||
package: str | None = None,
|
||
branch: str | None = None,
|
||
) -> int:
|
||
"""添加新的 session。"""
|
||
repo_root = get_repo_root()
|
||
warn_if_parallel_worktree(repo_root)
|
||
ensure_developer(repo_root)
|
||
|
||
developer = get_developer(repo_root)
|
||
if not developer:
|
||
print("错误:尚未初始化开发者身份。", file=sys.stderr)
|
||
return 1
|
||
|
||
dev_dir = get_workspace_dir(repo_root)
|
||
if not dev_dir:
|
||
print("错误:找不到 workspace 目录。", file=sys.stderr)
|
||
return 1
|
||
|
||
max_lines = get_max_journal_lines(repo_root)
|
||
|
||
index_file = dev_dir / "index.md"
|
||
today = datetime.now().strftime("%Y-%m-%d")
|
||
|
||
journal_file, current_num, current_lines = get_latest_journal_info(dev_dir)
|
||
current_session = get_current_session(index_file)
|
||
new_session = current_session + 1
|
||
|
||
session_content = generate_session_content(
|
||
new_session, title, commit, summary, today, package, branch,
|
||
changes=changes, extra_content=extra_content, tests=tests,
|
||
next_steps=next_steps,
|
||
)
|
||
content_lines = len(session_content.splitlines())
|
||
|
||
print("========================================", file=sys.stderr)
|
||
print("追加会话", file=sys.stderr)
|
||
print("========================================", file=sys.stderr)
|
||
print("", file=sys.stderr)
|
||
print(f"会话:{new_session}", file=sys.stderr)
|
||
print(f"标题:{title}", file=sys.stderr)
|
||
print(f"提交:{commit}", file=sys.stderr)
|
||
print("", file=sys.stderr)
|
||
print(f"当前 journal 文件:{FILE_JOURNAL_PREFIX}{current_num}.md", file=sys.stderr)
|
||
print(f"当前行数:{current_lines}", file=sys.stderr)
|
||
print(f"新增内容行数:{content_lines}", file=sys.stderr)
|
||
print(f"追加后总行数:{current_lines + content_lines}", file=sys.stderr)
|
||
print("", file=sys.stderr)
|
||
|
||
target_file = journal_file
|
||
target_num = current_num
|
||
|
||
if current_lines + content_lines > max_lines:
|
||
target_num = current_num + 1
|
||
print(f"[!] 超过 {max_lines} 行,创建 {FILE_JOURNAL_PREFIX}{target_num}.md", file=sys.stderr)
|
||
target_file = create_new_journal_file(dev_dir, target_num, developer, today, max_lines)
|
||
print(f"已创建:{target_file}", file=sys.stderr)
|
||
|
||
# 追加 session 内容。
|
||
if target_file:
|
||
with target_file.open("a", encoding="utf-8") as f:
|
||
f.write(session_content)
|
||
print(f"[OK] 已向 {target_file.name} 追加会话。", file=sys.stderr)
|
||
|
||
print("", file=sys.stderr)
|
||
|
||
# 更新 index.md。
|
||
active_file = f"{FILE_JOURNAL_PREFIX}{target_num}.md"
|
||
if not update_index(
|
||
index_file,
|
||
dev_dir,
|
||
title,
|
||
commit,
|
||
new_session,
|
||
active_file,
|
||
today,
|
||
branch,
|
||
):
|
||
return 1
|
||
|
||
print("", file=sys.stderr)
|
||
print("========================================", file=sys.stderr)
|
||
print(f"[OK] 会话 {new_session} 追加成功!", file=sys.stderr)
|
||
print("========================================", file=sys.stderr)
|
||
print("", file=sys.stderr)
|
||
print("已更新文件:", file=sys.stderr)
|
||
print(f" - {target_file.name if target_file else 'journal'}", file=sys.stderr)
|
||
print(" - index.md", file=sys.stderr)
|
||
|
||
# 自动提交 workspace 变更。
|
||
if auto_commit:
|
||
print("", file=sys.stderr)
|
||
_auto_commit_workspace(repo_root)
|
||
|
||
return 0
|
||
|
||
|
||
# =============================================================================
|
||
# 主入口
|
||
# =============================================================================
|
||
|
||
def main() -> int:
|
||
"""CLI 入口。"""
|
||
parser = argparse.ArgumentParser(
|
||
description="向 journal 文件追加会话并更新 index.md"
|
||
)
|
||
parser.add_argument("--title", required=True, help="会话标题")
|
||
parser.add_argument("--commit", default="-", help="逗号分隔的提交哈希")
|
||
parser.add_argument("--summary", default="未提供会话摘要。", help="简短摘要")
|
||
parser.add_argument("--content-file", help="详细内容文件路径")
|
||
parser.add_argument("--package", help="包名称标签(例如 cli、docs-site)")
|
||
parser.add_argument("--branch", help="分支名称(省略时自动探测)")
|
||
parser.add_argument("--change", action="append", help="主要变更条目(可重复)")
|
||
parser.add_argument("--test", action="append", help="测试条目(可重复)")
|
||
parser.add_argument("--next-step", action="append", help="后续步骤条目(可重复)")
|
||
parser.add_argument("--no-commit", action="store_true",
|
||
help="跳过 workspace 变更的自动提交")
|
||
parser.add_argument("--stdin", action="store_true",
|
||
help="从 stdin 读取额外内容(必须显式启用)")
|
||
|
||
args = parser.parse_args()
|
||
|
||
extra_content: str | None = None
|
||
if args.content_file:
|
||
content_path = Path(args.content_file)
|
||
if content_path.is_file():
|
||
extra_content = content_path.read_text(encoding="utf-8")
|
||
elif args.stdin:
|
||
extra_content = sys.stdin.read()
|
||
|
||
# 只加载一次活动任务,供 package 和 branch 解析共享。
|
||
repo_root = get_repo_root()
|
||
current = get_current_task(repo_root)
|
||
task_data = load_task(repo_root / current) if current else None
|
||
|
||
package = args.package
|
||
if package:
|
||
# CLI 来源:monorepo 中 fail-fast,single-repo 中忽略。
|
||
if not is_monorepo(repo_root):
|
||
print("警告:单仓库项目忽略 --package。", file=sys.stderr)
|
||
package = None
|
||
elif not validate_package(package, repo_root):
|
||
packages = get_packages(repo_root)
|
||
available = ", ".join(sorted(packages.keys())) if packages else "(无)"
|
||
print(f"错误:未知 package '{package}'。可用值:{available}", file=sys.stderr)
|
||
return 1
|
||
else:
|
||
# 推断顺序:活动任务的 task.json.package → default_package → None。
|
||
task_package = task_data.package if task_data else None
|
||
package = resolve_package(task_package, repo_root)
|
||
|
||
branch = resolve_session_branch(repo_root, args.branch, task_data)
|
||
|
||
return add_session(
|
||
args.title, args.commit, args.summary,
|
||
changes=args.change, extra_content=extra_content, tests=args.test,
|
||
next_steps=args.next_step,
|
||
auto_commit=not args.no_commit,
|
||
package=package,
|
||
branch=branch,
|
||
)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|