feat: normalize review bundle into review document
This commit is contained in:
@@ -6,6 +6,7 @@ pub mod cli;
|
||||
pub mod error;
|
||||
pub mod gitea;
|
||||
pub mod model;
|
||||
pub mod normalize;
|
||||
pub mod output;
|
||||
pub mod render;
|
||||
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
|
||||
use crate::gitea::dto::{ChangedFileDto, CommitDto, PullBundleDto, ReviewCommentDto, ReviewDto};
|
||||
use crate::model::{
|
||||
CommentItem, CommentThread, CommitItem, DiffStat, FileStat, PrMeta, PrReviewDocument,
|
||||
ReviewItem,
|
||||
};
|
||||
|
||||
pub fn normalize_bundle(repo: &str, bundle: PullBundleDto) -> PrReviewDocument {
|
||||
let PullBundleDto {
|
||||
pull,
|
||||
reviews,
|
||||
comments,
|
||||
commits,
|
||||
files,
|
||||
} = bundle;
|
||||
|
||||
let mut threads = normalize_threads(comments);
|
||||
threads.sort_by(|a, b| {
|
||||
comment_item_sort_key(&a.root_comment).cmp(&comment_item_sort_key(&b.root_comment))
|
||||
});
|
||||
|
||||
let commits = normalize_commits(commits);
|
||||
let reviews = normalize_reviews(reviews);
|
||||
let diff_stat = normalize_diff_stat(&pull, files);
|
||||
|
||||
PrReviewDocument {
|
||||
meta: PrMeta {
|
||||
repo: repo.to_string(),
|
||||
pr_index: pull.number,
|
||||
title: pull.title,
|
||||
state: pull.state,
|
||||
author: pull.user.login,
|
||||
base_branch: pull.base.ref_name,
|
||||
head_branch: pull.head.ref_name,
|
||||
created_at: pull.created_at,
|
||||
updated_at: pull.updated_at,
|
||||
merged_at: pull.merged_at,
|
||||
},
|
||||
commits,
|
||||
diff_stat,
|
||||
reviews,
|
||||
threads,
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_threads(comments: Vec<ReviewCommentDto>) -> Vec<CommentThread> {
|
||||
let mut grouped: BTreeMap<String, Vec<ReviewCommentDto>> = BTreeMap::new();
|
||||
|
||||
for comment in comments {
|
||||
let key = thread_key(&comment);
|
||||
grouped.entry(key).or_default().push(comment);
|
||||
}
|
||||
|
||||
grouped
|
||||
.into_iter()
|
||||
.map(|(thread_id, mut group)| {
|
||||
group.sort_by(|a, b| comment_sort_key(a).cmp(&comment_sort_key(b)));
|
||||
|
||||
let root = group.remove(0);
|
||||
let file_path = root
|
||||
.path
|
||||
.clone()
|
||||
.or_else(|| group.iter().find_map(|comment| comment.path.clone()));
|
||||
let line = root
|
||||
.line
|
||||
.or_else(|| group.iter().find_map(|comment| comment.line));
|
||||
|
||||
CommentThread {
|
||||
thread_id,
|
||||
file_path,
|
||||
line,
|
||||
root_comment: to_comment_item(root),
|
||||
replies: group.into_iter().map(to_comment_item).collect(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn thread_key(comment: &ReviewCommentDto) -> String {
|
||||
match comment.pull_request_review_id {
|
||||
Some(review_id) => format!(
|
||||
"review-{review_id}-{path}-{line}-{position}-{commit}",
|
||||
path = comment.path.as_deref().unwrap_or(""),
|
||||
line = comment
|
||||
.line
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_default(),
|
||||
position = comment
|
||||
.original_position
|
||||
.or(comment.position)
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_default(),
|
||||
commit = comment
|
||||
.original_commit_id
|
||||
.as_deref()
|
||||
.or(comment.commit_id.as_deref())
|
||||
.unwrap_or("")
|
||||
),
|
||||
None => format!("comment-{}", comment.id),
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_commits(commits: Vec<CommitDto>) -> Vec<CommitItem> {
|
||||
let mut commits = commits;
|
||||
commits.sort_by(|a, b| {
|
||||
commit_sort_key(a)
|
||||
.cmp(&commit_sort_key(b))
|
||||
.then_with(|| a.sha.cmp(&b.sha))
|
||||
});
|
||||
|
||||
commits.into_iter().map(to_commit_item).collect()
|
||||
}
|
||||
|
||||
fn normalize_reviews(reviews: Vec<ReviewDto>) -> Vec<ReviewItem> {
|
||||
let mut reviews = reviews;
|
||||
reviews.sort_by(|a, b| {
|
||||
review_sort_key(a)
|
||||
.cmp(&review_sort_key(b))
|
||||
.then_with(|| a.id.cmp(&b.id))
|
||||
});
|
||||
|
||||
reviews
|
||||
.into_iter()
|
||||
.map(|review| ReviewItem {
|
||||
id: review.id,
|
||||
state: review.state,
|
||||
reviewer: review.user.login,
|
||||
submitted_at: review.submitted_at,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn normalize_diff_stat(pull: &crate::gitea::dto::PullDto, files: Vec<ChangedFileDto>) -> DiffStat {
|
||||
let file_stats: Vec<FileStat> = files
|
||||
.into_iter()
|
||||
.map(|file| FileStat {
|
||||
path: file.filename,
|
||||
additions: file.additions,
|
||||
deletions: file.deletions,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let files_changed = if file_stats.is_empty() {
|
||||
pull.changed_files
|
||||
.and_then(|value| usize::try_from(value).ok())
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
file_stats.len()
|
||||
};
|
||||
|
||||
let additions: i64 = if file_stats.is_empty() {
|
||||
pull.additions.unwrap_or_default()
|
||||
} else {
|
||||
file_stats.iter().map(|file| file.additions).sum()
|
||||
};
|
||||
let deletions: i64 = if file_stats.is_empty() {
|
||||
pull.deletions.unwrap_or_default()
|
||||
} else {
|
||||
file_stats.iter().map(|file| file.deletions).sum()
|
||||
};
|
||||
|
||||
DiffStat {
|
||||
files_changed,
|
||||
additions,
|
||||
deletions,
|
||||
files: file_stats,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_commit_item(commit: CommitDto) -> CommitItem {
|
||||
let title = commit
|
||||
.commit
|
||||
.message
|
||||
.lines()
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let short_sha = commit.sha.chars().take(7).collect::<String>();
|
||||
let author = commit
|
||||
.commit
|
||||
.author
|
||||
.as_ref()
|
||||
.map(|author| author.name.clone())
|
||||
.unwrap_or_default();
|
||||
let date = commit
|
||||
.commit
|
||||
.author
|
||||
.as_ref()
|
||||
.map(|author| author.date.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
CommitItem {
|
||||
sha: commit.sha,
|
||||
short_sha,
|
||||
title,
|
||||
author,
|
||||
date,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_comment_item(comment: ReviewCommentDto) -> CommentItem {
|
||||
CommentItem {
|
||||
id: comment.id,
|
||||
user: comment.user.login,
|
||||
created_at: comment.created_at,
|
||||
body: comment.body,
|
||||
}
|
||||
}
|
||||
|
||||
fn review_sort_key(review: &ReviewDto) -> (i64, i64) {
|
||||
(timestamp_millis(review.submitted_at.as_deref()), review.id)
|
||||
}
|
||||
|
||||
fn commit_sort_key(commit: &CommitDto) -> (i64, String) {
|
||||
(
|
||||
timestamp_millis(Some(
|
||||
commit
|
||||
.commit
|
||||
.author
|
||||
.as_ref()
|
||||
.map(|author| author.date.as_str())
|
||||
.unwrap_or_default(),
|
||||
)),
|
||||
commit.sha.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
fn comment_sort_key(comment: &ReviewCommentDto) -> (i64, i64) {
|
||||
(
|
||||
timestamp_millis(Some(comment.created_at.as_str())),
|
||||
comment.id,
|
||||
)
|
||||
}
|
||||
|
||||
fn comment_item_sort_key(comment: &CommentItem) -> (i64, i64) {
|
||||
(
|
||||
timestamp_millis(Some(comment.created_at.as_str())),
|
||||
comment.id,
|
||||
)
|
||||
}
|
||||
|
||||
fn timestamp_millis(input: Option<&str>) -> i64 {
|
||||
input
|
||||
.and_then(|value| DateTime::parse_from_rfc3339(value).ok())
|
||||
.map(|dt: DateTime<FixedOffset>| dt.timestamp_millis())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
Reference in New Issue
Block a user