From c4fa0eadcc49b88ea7a3ba2fe5e2b5fe7d49592c Mon Sep 17 00:00:00 2001 From: Origami404 Date: Wed, 8 Apr 2026 23:59:55 +0800 Subject: [PATCH] feat: enforce output version v1 and add version subcommand --- AGENTS.md | 4 +++ README.en.md | 8 +++++ README.md | 8 +++++ src/cli.rs | 1 + src/lib.rs | 5 ++++ src/model.rs | 3 +- src/normalize.rs | 4 ++- src/render/json.rs | 11 ++++++- src/render/markdown.rs | 4 +-- tests/cli_parse_tests.rs | 9 ++++++ tests/e2e_smoke_tests.rs | 55 ++++++++++++++++++++++++++++++++++ tests/normalize_tests.rs | 2 +- tests/render_json_tests.rs | 6 ++-- tests/render_markdown_tests.rs | 7 +++-- 14 files changed, 117 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 19f1bef..6eb11f0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1 +1,5 @@ refs/ 里有 gitea API 的 spec + +版本号规则: +- 当前输出文档版本号固定为 `v1` +- 如果用户没有明确要求 bump 版本号,禁止修改版本号 diff --git a/README.en.md b/README.en.md index 3cfb668..e230835 100644 --- a/README.en.md +++ b/README.en.md @@ -54,12 +54,20 @@ gitea-pr-review render-md --in pr.json gitea-pr-review render-md --in pr.json --out pr.md ``` +### 3) Show current document version + +```bash +gitea-pr-review version +# v1 +``` + ## Markdown Output Shape (Example) ````md # `#` > Numbering: Review `.`; Comment `..`; Reply `...` +> version: v1 > fetched at: diff --git a/README.md b/README.md index a9429b9..4cda558 100644 --- a/README.md +++ b/README.md @@ -54,12 +54,20 @@ gitea-pr-review render-md --in pr.json gitea-pr-review render-md --in pr.json --out pr.md ``` +### 3) 查看当前文档版本 + +```bash +gitea-pr-review version +# v1 +``` + ## Markdown 输出结构(示例) ````md # `#` > 编号规则:Review `.`;Comment `..`;Reply `...` +> version: v1 > fetched at: diff --git a/src/cli.rs b/src/cli.rs index 3b29805..6eca915 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -18,6 +18,7 @@ pub struct Cli { pub enum Commands { Fetch(FetchArgs), RenderMd(RenderMdArgs), + Version, } #[derive(Debug, clap::Args)] diff --git a/src/lib.rs b/src/lib.rs index 58eb191..3d6b829 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,8 @@ use crate::output::write_output; use crate::render::json::{parse_json, render_json}; use crate::render::markdown::render_markdown; +pub const OUTPUT_VERSION: &str = "v1"; + pub fn run() -> anyhow::Result<()> { let cli = Cli::parse(); @@ -29,6 +31,9 @@ pub fn run() -> anyhow::Result<()> { let md = render_markdown(&doc); write_output(args.out.as_deref().map(Path::new), &md)?; } + Commands::Version => { + write_output(None, OUTPUT_VERSION)?; + } Commands::Fetch(args) => { let token = required_env("GITEA_PR_CLI_API_TOKEN")?; let base_url = required_env("GITEA_PR_CLI_URL")?; diff --git a/src/model.rs b/src/model.rs index f4318b4..8bf1029 100644 --- a/src/model.rs +++ b/src/model.rs @@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PrReviewDocument { + pub version: String, pub meta: PrMeta, pub commits: Vec, pub diff_stat: DiffStat, @@ -15,7 +16,7 @@ pub struct PrMeta { pub pr_index: i64, pub title: String, pub description: Option, - pub fetched_at: Option, + pub fetched_at: String, pub state: String, pub author: String, pub base_branch: String, diff --git a/src/normalize.rs b/src/normalize.rs index 0f4a269..5b9004d 100644 --- a/src/normalize.rs +++ b/src/normalize.rs @@ -4,6 +4,7 @@ use std::convert::TryFrom; use chrono::{DateTime, FixedOffset}; use crate::gitea::dto::{ChangedFileDto, CommitDto, PullBundleDto, ReviewCommentDto, ReviewDto}; +use crate::OUTPUT_VERSION; use crate::model::{ CommentItem, CommentThread, CommitItem, DiffStat, FileStat, PrMeta, PrReviewDocument, ReviewItem, @@ -28,12 +29,13 @@ pub fn normalize_bundle(repo: &str, fetched_at: String, bundle: PullBundleDto) - let diff_stat = normalize_diff_stat(&pull, files); PrReviewDocument { + version: OUTPUT_VERSION.to_string(), meta: PrMeta { repo: repo.to_string(), pr_index: pull.number, title: pull.title, description: pull.body, - fetched_at: Some(fetched_at), + fetched_at, state: pull.state, author: pull.user.login, base_branch: pull.base.ref_name, diff --git a/src/render/json.rs b/src/render/json.rs index 56f90f3..663f23a 100644 --- a/src/render/json.rs +++ b/src/render/json.rs @@ -1,9 +1,18 @@ use crate::model::PrReviewDocument; +use crate::OUTPUT_VERSION; pub fn render_json(doc: &PrReviewDocument) -> anyhow::Result { Ok(serde_json::to_string_pretty(doc)?) } pub fn parse_json(input: &str) -> anyhow::Result { - Ok(serde_json::from_str(input)?) + let doc: PrReviewDocument = serde_json::from_str(input)?; + if doc.version != OUTPUT_VERSION { + anyhow::bail!( + "unsupported document version: {}, expected {}", + doc.version, + OUTPUT_VERSION + ); + } + Ok(doc) } diff --git a/src/render/markdown.rs b/src/render/markdown.rs index 8e7c1ad..ebf9eb0 100644 --- a/src/render/markdown.rs +++ b/src/render/markdown.rs @@ -65,8 +65,8 @@ pub fn render_markdown(doc: &PrReviewDocument) -> String { out.push_str( "> 编号规则:Review `.`;Comment `..`;Reply `...`\n\n", ); - let fetched_at = doc.meta.fetched_at.as_deref().unwrap_or("unknown"); - out.push_str(&format!("> fetched at: {fetched_at}\n\n")); + out.push_str(&format!("> version: {}\n\n", doc.version)); + out.push_str(&format!("> fetched at: {}\n\n", doc.meta.fetched_at)); let pr_description = doc .meta .description diff --git a/tests/cli_parse_tests.rs b/tests/cli_parse_tests.rs index 6aedaf0..f276c62 100644 --- a/tests/cli_parse_tests.rs +++ b/tests/cli_parse_tests.rs @@ -26,3 +26,12 @@ fn parse_render_md_requires_input() { _ => panic!("expected render-md"), } } + +#[test] +fn parse_version_subcommand() { + let cli = Cli::try_parse_from(["gitea-pr-review", "version"]).unwrap(); + match cli.command { + Commands::Version => {} + _ => panic!("expected version"), + } +} diff --git a/tests/e2e_smoke_tests.rs b/tests/e2e_smoke_tests.rs index ad601b6..ec03dc4 100644 --- a/tests/e2e_smoke_tests.rs +++ b/tests/e2e_smoke_tests.rs @@ -8,6 +8,7 @@ fn render_md_reads_json_and_outputs_markdown_to_stdout() { std::fs::write( input.path(), r#"{ + "version": "v1", "meta": { "repo": "org/repo", "pr_index": 1, @@ -52,6 +53,7 @@ fn render_md_writes_to_out_file_when_requested() { std::fs::write( input.path(), r#"{ + "version": "v1", "meta": { "repo": "org/repo", "pr_index": 2, @@ -291,6 +293,59 @@ fn fetch_writes_json_to_out_file_when_requested() { let written = std::fs::read_to_string(output.path()).unwrap(); assert!(written.contains("\"repo\": \"org/repo\"")); assert!(written.contains("\"pr_index\": 8")); + assert!(written.contains("\"version\": \"v1\"")); assert!(written.contains("\"fetched_at\":")); assert!(written.contains("\"head_branch\": \"feature/y\"")); } + +#[test] +fn render_md_fails_when_version_mismatch() { + let input = NamedTempFile::new().unwrap(); + std::fs::write( + input.path(), + r#"{ + "version": "v2", + "meta": { + "repo": "org/repo", + "pr_index": 3, + "title": "t3", + "description": "PR body", + "fetched_at": "2026-04-08T12:34:56Z", + "state": "open", + "author": "a", + "base_branch": "main", + "head_branch": "f", + "created_at": "2026-04-08T10:00:00Z", + "updated_at": "2026-04-08T11:00:00Z", + "merged_at": null + }, + "commits": [], + "diff_stat": { + "files_changed": 0, + "additions": 0, + "deletions": 0, + "files": [] + }, + "reviews": [], + "threads": [] +}"#, + ) + .unwrap(); + + Command::cargo_bin("gitea-pr-review") + .unwrap() + .args(["render-md", "--in", input.path().to_str().unwrap()]) + .assert() + .failure(); +} + +#[test] +fn version_subcommand_prints_current_version() { + let assert = Command::cargo_bin("gitea-pr-review") + .unwrap() + .arg("version") + .assert() + .success(); + let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap(); + assert_eq!(stdout.trim(), "v1"); +} diff --git a/tests/normalize_tests.rs b/tests/normalize_tests.rs index 021441b..8838530 100644 --- a/tests/normalize_tests.rs +++ b/tests/normalize_tests.rs @@ -145,7 +145,7 @@ fn normalize_groups_replies_and_sorts_threads_by_time() { assert_eq!(doc.meta.repo, "org/repo"); assert_eq!(doc.meta.pr_index, 42); - assert_eq!(doc.meta.fetched_at.as_deref(), Some("2026-04-08T12:34:56Z")); + assert_eq!(doc.meta.fetched_at, "2026-04-08T12:34:56Z"); assert_eq!(doc.meta.base_branch, "main"); assert_eq!(doc.meta.head_branch, "feature/x"); diff --git a/tests/render_json_tests.rs b/tests/render_json_tests.rs index a76d0de..1438f06 100644 --- a/tests/render_json_tests.rs +++ b/tests/render_json_tests.rs @@ -5,12 +5,13 @@ use gitea_pr_review::model::{ #[test] fn model_json_roundtrip() { let doc = PrReviewDocument { + version: "v1".into(), meta: PrMeta { repo: "org/repo".into(), pr_index: 9, title: "feat: x".into(), description: Some("desc".into()), - fetched_at: Some("2026-04-08T12:34:56Z".into()), + fetched_at: "2026-04-08T12:34:56Z".into(), state: "open".into(), author: "alice".into(), base_branch: "main".into(), @@ -50,7 +51,8 @@ fn model_json_roundtrip() { let encoded = serde_json::to_string(&doc).unwrap(); let decoded: PrReviewDocument = serde_json::from_str(&encoded).unwrap(); + assert_eq!(decoded.version, "v1"); assert_eq!(decoded.meta.repo, "org/repo"); - assert_eq!(decoded.meta.fetched_at.as_deref(), Some("2026-04-08T12:34:56Z")); + assert_eq!(decoded.meta.fetched_at, "2026-04-08T12:34:56Z"); assert_eq!(decoded.threads[0].root_comment.body, "hello"); } diff --git a/tests/render_markdown_tests.rs b/tests/render_markdown_tests.rs index cf21521..71bfeca 100644 --- a/tests/render_markdown_tests.rs +++ b/tests/render_markdown_tests.rs @@ -7,12 +7,13 @@ use gitea_pr_review::render::markdown::render_markdown; #[test] fn render_markdown_includes_expected_sections_and_preserves_comment_markdown() { let doc = PrReviewDocument { + version: "v1".into(), meta: PrMeta { repo: "org/repo".into(), pr_index: 7, title: "Fix parser".into(), description: Some("PR body".into()), - fetched_at: Some("2026-04-08T12:34:56Z".into()), + fetched_at: "2026-04-08T12:34:56Z".into(), state: "open".into(), author: "alice".into(), base_branch: "main".into(), @@ -73,6 +74,7 @@ fn render_markdown_includes_expected_sections_and_preserves_comment_markdown() { assert!(md.contains("# org/repo `#7` Fix parser")); assert!(md.contains("## Metadata")); + assert!(md.contains("> version: v1")); assert!(md.contains("> fetched at: 2026-04-08T12:34:56Z")); assert!(md.contains("## Commits")); assert!(md.contains("## Diff Stat")); @@ -96,12 +98,13 @@ fn render_markdown_includes_expected_sections_and_preserves_comment_markdown() { fn render_markdown_uses_minimal_fence_for_plain_text() { let body = "plain text"; let doc = PrReviewDocument { + version: "v1".into(), meta: PrMeta { repo: "org/repo".into(), pr_index: 1, title: "T".into(), description: None, - fetched_at: None, + fetched_at: "2026-04-08T12:34:56Z".into(), state: "open".into(), author: "alice".into(), base_branch: "main".into(),