feat: add fetched-at timestamp to markdown header and json meta

This commit is contained in:
2026-04-08 23:56:01 +08:00
parent ed67986320
commit a6daaff0fa
10 changed files with 23 additions and 5 deletions
+1
View File
@@ -60,6 +60,7 @@ gitea-pr-review render-md --in pr.json --out pr.md
# <repo> `#<pr-index>` <pr-title> # <repo> `#<pr-index>` <pr-title>
> Numbering: Review `<pr>.<review>`; Comment `<pr>.<review>.<comment>`; Reply `<pr>.<review>.<comment>.<reply>` > Numbering: Review `<pr>.<review>`; Comment `<pr>.<review>.<comment>`; Reply `<pr>.<review>.<comment>.<reply>`
> fetched at: <fetch-time-rfc3339>
<pr description> <pr description>
+1
View File
@@ -60,6 +60,7 @@ gitea-pr-review render-md --in pr.json --out pr.md
# <repo> `#<pr-index>` <pr-title> # <repo> `#<pr-index>` <pr-title>
> 编号规则:Review `<pr>.<review>`Comment `<pr>.<review>.<comment>`Reply `<pr>.<review>.<comment>.<reply>` > 编号规则:Review `<pr>.<review>`Comment `<pr>.<review>.<comment>`Reply `<pr>.<review>.<comment>.<reply>`
> fetched at: <fetch-time-rfc3339>
<pr 描述> <pr 描述>
+3 -1
View File
@@ -1,6 +1,7 @@
use std::env; use std::env;
use std::path::Path; use std::path::Path;
use chrono::Utc;
use clap::Parser; use clap::Parser;
pub mod cli; pub mod cli;
@@ -35,7 +36,8 @@ pub fn run() -> anyhow::Result<()> {
let client = GiteaClient::new(base_url, token); let client = GiteaClient::new(base_url, token);
let bundle = client.fetch_pr_bundle(&repo, args.pr_index)?; let bundle = client.fetch_pr_bundle(&repo, args.pr_index)?;
let doc = normalize_bundle(&repo, bundle); let fetched_at = Utc::now().to_rfc3339();
let doc = normalize_bundle(&repo, fetched_at, bundle);
let rendered = match args.format { let rendered = match args.format {
OutputFormat::Markdown => render_markdown(&doc), OutputFormat::Markdown => render_markdown(&doc),
+1
View File
@@ -15,6 +15,7 @@ pub struct PrMeta {
pub pr_index: i64, pub pr_index: i64,
pub title: String, pub title: String,
pub description: Option<String>, pub description: Option<String>,
pub fetched_at: Option<String>,
pub state: String, pub state: String,
pub author: String, pub author: String,
pub base_branch: String, pub base_branch: String,
+2 -1
View File
@@ -9,7 +9,7 @@ use crate::model::{
ReviewItem, ReviewItem,
}; };
pub fn normalize_bundle(repo: &str, bundle: PullBundleDto) -> PrReviewDocument { pub fn normalize_bundle(repo: &str, fetched_at: String, bundle: PullBundleDto) -> PrReviewDocument {
let PullBundleDto { let PullBundleDto {
pull, pull,
reviews, reviews,
@@ -33,6 +33,7 @@ pub fn normalize_bundle(repo: &str, bundle: PullBundleDto) -> PrReviewDocument {
pr_index: pull.number, pr_index: pull.number,
title: pull.title, title: pull.title,
description: pull.body, description: pull.body,
fetched_at: Some(fetched_at),
state: pull.state, state: pull.state,
author: pull.user.login, author: pull.user.login,
base_branch: pull.base.ref_name, base_branch: pull.base.ref_name,
+2
View File
@@ -65,6 +65,8 @@ pub fn render_markdown(doc: &PrReviewDocument) -> String {
out.push_str( out.push_str(
"> 编号规则:Review `<pr>.<review>`Comment `<pr>.<review>.<comment>`Reply `<pr>.<review>.<comment>.<reply>`\n\n", "> 编号规则: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"));
let pr_description = doc let pr_description = doc
.meta .meta
.description .description
+4
View File
@@ -13,6 +13,7 @@ fn render_md_reads_json_and_outputs_markdown_to_stdout() {
"pr_index": 1, "pr_index": 1,
"title": "t", "title": "t",
"description": "PR body", "description": "PR body",
"fetched_at": "2026-04-08T12:34:56Z",
"state": "open", "state": "open",
"author": "a", "author": "a",
"base_branch": "main", "base_branch": "main",
@@ -56,6 +57,7 @@ fn render_md_writes_to_out_file_when_requested() {
"pr_index": 2, "pr_index": 2,
"title": "t2", "title": "t2",
"description": "PR body", "description": "PR body",
"fetched_at": "2026-04-08T12:34:56Z",
"state": "open", "state": "open",
"author": "a", "author": "a",
"base_branch": "main", "base_branch": "main",
@@ -220,6 +222,7 @@ fn fetch_writes_markdown_to_stdout_by_default() {
let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap(); let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
assert!(stdout.contains("# org/repo `#7` Fix parser")); assert!(stdout.contains("# org/repo `#7` Fix parser"));
assert!(stdout.contains("> fetched at: "));
assert!(stdout.contains("## Commits")); assert!(stdout.contains("## Commits"));
assert!(stdout.contains("## Diff Stat")); assert!(stdout.contains("## Diff Stat"));
assert!(stdout.contains("## Review 7.1 (COMMENT)")); assert!(stdout.contains("## Review 7.1 (COMMENT)"));
@@ -288,5 +291,6 @@ fn fetch_writes_json_to_out_file_when_requested() {
let written = std::fs::read_to_string(output.path()).unwrap(); let written = std::fs::read_to_string(output.path()).unwrap();
assert!(written.contains("\"repo\": \"org/repo\"")); assert!(written.contains("\"repo\": \"org/repo\""));
assert!(written.contains("\"pr_index\": 8")); assert!(written.contains("\"pr_index\": 8"));
assert!(written.contains("\"fetched_at\":"));
assert!(written.contains("\"head_branch\": \"feature/y\"")); assert!(written.contains("\"head_branch\": \"feature/y\""));
} }
+4 -3
View File
@@ -141,10 +141,11 @@ fn normalize_groups_replies_and_sorts_threads_by_time() {
], ],
}; };
let doc = normalize_bundle("org/repo", bundle); let doc = normalize_bundle("org/repo", "2026-04-08T12:34:56Z".into(), bundle);
assert_eq!(doc.meta.repo, "org/repo"); assert_eq!(doc.meta.repo, "org/repo");
assert_eq!(doc.meta.pr_index, 42); 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.base_branch, "main"); assert_eq!(doc.meta.base_branch, "main");
assert_eq!(doc.meta.head_branch, "feature/x"); assert_eq!(doc.meta.head_branch, "feature/x");
@@ -241,7 +242,7 @@ fn normalize_does_not_merge_unrelated_comments_on_same_file() {
files: vec![], files: vec![],
}; };
let doc = normalize_bundle("org/repo", bundle); let doc = normalize_bundle("org/repo", "2026-04-08T12:34:56Z".into(), bundle);
assert_eq!(doc.threads.len(), 2); assert_eq!(doc.threads.len(), 2);
assert_eq!(doc.threads[0].root_comment.id, 10); assert_eq!(doc.threads[0].root_comment.id, 10);
@@ -297,7 +298,7 @@ fn normalize_uses_non_zero_position_when_line_is_missing() {
files: vec![], files: vec![],
}; };
let doc = normalize_bundle("org/repo", bundle); let doc = normalize_bundle("org/repo", "2026-04-08T12:34:56Z".into(), bundle);
assert_eq!(doc.threads.len(), 1); assert_eq!(doc.threads.len(), 1);
assert_eq!(doc.threads[0].root_comment.path.as_deref(), Some("src/main.rs")); assert_eq!(doc.threads[0].root_comment.path.as_deref(), Some("src/main.rs"));
assert_eq!(doc.threads[0].root_comment.line, Some(41)); assert_eq!(doc.threads[0].root_comment.line, Some(41));
+2
View File
@@ -10,6 +10,7 @@ fn model_json_roundtrip() {
pr_index: 9, pr_index: 9,
title: "feat: x".into(), title: "feat: x".into(),
description: Some("desc".into()), description: Some("desc".into()),
fetched_at: Some("2026-04-08T12:34:56Z".into()),
state: "open".into(), state: "open".into(),
author: "alice".into(), author: "alice".into(),
base_branch: "main".into(), base_branch: "main".into(),
@@ -50,5 +51,6 @@ fn model_json_roundtrip() {
let encoded = serde_json::to_string(&doc).unwrap(); let encoded = serde_json::to_string(&doc).unwrap();
let decoded: PrReviewDocument = serde_json::from_str(&encoded).unwrap(); let decoded: PrReviewDocument = serde_json::from_str(&encoded).unwrap();
assert_eq!(decoded.meta.repo, "org/repo"); assert_eq!(decoded.meta.repo, "org/repo");
assert_eq!(decoded.meta.fetched_at.as_deref(), Some("2026-04-08T12:34:56Z"));
assert_eq!(decoded.threads[0].root_comment.body, "hello"); assert_eq!(decoded.threads[0].root_comment.body, "hello");
} }
+3
View File
@@ -12,6 +12,7 @@ fn render_markdown_includes_expected_sections_and_preserves_comment_markdown() {
pr_index: 7, pr_index: 7,
title: "Fix parser".into(), title: "Fix parser".into(),
description: Some("PR body".into()), description: Some("PR body".into()),
fetched_at: Some("2026-04-08T12:34:56Z".into()),
state: "open".into(), state: "open".into(),
author: "alice".into(), author: "alice".into(),
base_branch: "main".into(), base_branch: "main".into(),
@@ -72,6 +73,7 @@ fn render_markdown_includes_expected_sections_and_preserves_comment_markdown() {
assert!(md.contains("# org/repo `#7` Fix parser")); assert!(md.contains("# org/repo `#7` Fix parser"));
assert!(md.contains("## Metadata")); assert!(md.contains("## Metadata"));
assert!(md.contains("> fetched at: 2026-04-08T12:34:56Z"));
assert!(md.contains("## Commits")); assert!(md.contains("## Commits"));
assert!(md.contains("## Diff Stat")); assert!(md.contains("## Diff Stat"));
assert!(md.contains("> 编号规则:Review `<pr>.<review>`Comment `<pr>.<review>.<comment>`Reply `<pr>.<review>.<comment>.<reply>`")); assert!(md.contains("> 编号规则:Review `<pr>.<review>`Comment `<pr>.<review>.<comment>`Reply `<pr>.<review>.<comment>.<reply>`"));
@@ -99,6 +101,7 @@ fn render_markdown_uses_minimal_fence_for_plain_text() {
pr_index: 1, pr_index: 1,
title: "T".into(), title: "T".into(),
description: None, description: None,
fetched_at: None,
state: "open".into(), state: "open".into(),
author: "alice".into(), author: "alice".into(),
base_branch: "main".into(), base_branch: "main".into(),