feat: enforce output version v1 and add version subcommand
This commit is contained in:
@@ -1 +1,5 @@
|
||||
refs/ 里有 gitea API 的 spec
|
||||
|
||||
版本号规则:
|
||||
- 当前输出文档版本号固定为 `v1`
|
||||
- 如果用户没有明确要求 bump 版本号,禁止修改版本号
|
||||
|
||||
@@ -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
|
||||
# <repo> `#<pr-index>` <pr-title>
|
||||
|
||||
> Numbering: Review `<pr>.<review>`; Comment `<pr>.<review>.<comment>`; Reply `<pr>.<review>.<comment>.<reply>`
|
||||
> version: v1
|
||||
> fetched at: <fetch-time-rfc3339>
|
||||
|
||||
<pr description>
|
||||
|
||||
@@ -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
|
||||
# <repo> `#<pr-index>` <pr-title>
|
||||
|
||||
> 编号规则:Review `<pr>.<review>`;Comment `<pr>.<review>.<comment>`;Reply `<pr>.<review>.<comment>.<reply>`
|
||||
> version: v1
|
||||
> fetched at: <fetch-time-rfc3339>
|
||||
|
||||
<pr 描述>
|
||||
|
||||
@@ -18,6 +18,7 @@ pub struct Cli {
|
||||
pub enum Commands {
|
||||
Fetch(FetchArgs),
|
||||
RenderMd(RenderMdArgs),
|
||||
Version,
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Args)]
|
||||
|
||||
@@ -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")?;
|
||||
|
||||
+2
-1
@@ -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<CommitItem>,
|
||||
pub diff_stat: DiffStat,
|
||||
@@ -15,7 +16,7 @@ pub struct PrMeta {
|
||||
pub pr_index: i64,
|
||||
pub title: String,
|
||||
pub description: Option<String>,
|
||||
pub fetched_at: Option<String>,
|
||||
pub fetched_at: String,
|
||||
pub state: String,
|
||||
pub author: String,
|
||||
pub base_branch: String,
|
||||
|
||||
+3
-1
@@ -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,
|
||||
|
||||
+10
-1
@@ -1,9 +1,18 @@
|
||||
use crate::model::PrReviewDocument;
|
||||
use crate::OUTPUT_VERSION;
|
||||
|
||||
pub fn render_json(doc: &PrReviewDocument) -> anyhow::Result<String> {
|
||||
Ok(serde_json::to_string_pretty(doc)?)
|
||||
}
|
||||
|
||||
pub fn parse_json(input: &str) -> anyhow::Result<PrReviewDocument> {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -65,8 +65,8 @@ pub fn render_markdown(doc: &PrReviewDocument) -> String {
|
||||
out.push_str(
|
||||
"> 编号规则:Review `<pr>.<review>`;Comment `<pr>.<review>.<comment>`;Reply `<pr>.<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
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user