From 5e5f17e07bc1d76a6b8263707f9fcdefc6d96089 Mon Sep 17 00:00:00 2001 From: Leike Date: Thu, 3 Mar 2022 15:19:26 +0800 Subject: [PATCH 1/2] =?UTF-8?q?specdiff=20=E5=AE=8C=E5=96=84=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E8=A1=8C=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- specdiff/src/error.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 specdiff/src/error.rs diff --git a/specdiff/src/error.rs b/specdiff/src/error.rs new file mode 100644 index 00000000..a8279359 --- /dev/null +++ b/specdiff/src/error.rs @@ -0,0 +1,12 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum SpecError { + #[error("Download spec file error")] + DownloadError(#[from] reqwest::Error), + #[error("Parsing toml file error")] + ParseError(#[from] toml::de::Error), + #[error("I/O error")] + IoError(#[from] std::io::Error), + +} -- Gitee From da39f8a72403c4bc944b1f986fa2462597a8fe72 Mon Sep 17 00:00:00 2001 From: Leike Date: Thu, 3 Mar 2022 15:32:15 +0800 Subject: [PATCH 2/2] =?UTF-8?q?specdiff=20=E5=AE=8C=E5=96=84=E7=BB=86?= =?UTF-8?q?=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- specdiff/Cargo.toml | 4 +- specdiff/src/lib.rs | 214 +++++++++++++++++++++-------- specdiff/src/main.rs | 37 +++-- specdiff/tests/integration_test.rs | 10 +- 4 files changed, 195 insertions(+), 70 deletions(-) diff --git a/specdiff/Cargo.toml b/specdiff/Cargo.toml index 6a3630c6..9b47ba5c 100644 --- a/specdiff/Cargo.toml +++ b/specdiff/Cargo.toml @@ -14,4 +14,6 @@ markdown-gen = "1.2.1" colored = "2.0.0" console = "0.15.0" toml = "0.5.8" -serde = { version = "1.0", features = ["derive"] } \ No newline at end of file +serde = { version = "1.0", features = ["derive"] } +thiserror = "1" +chrono = "0.4" \ No newline at end of file diff --git a/specdiff/src/lib.rs b/specdiff/src/lib.rs index 276d069d..03bc2300 100644 --- a/specdiff/src/lib.rs +++ b/specdiff/src/lib.rs @@ -1,18 +1,25 @@ #![allow(unused_imports)] #![allow(dead_code)] -use clap::Parser; +pub use clap::Parser; +use reqwest::Response; use similar::{ChangeTag, TextDiff}; use markdown_gen::markdown::Markdown; use console::{style, Style}; pub use tokio::try_join; use serde::Deserialize; +pub use chrono::prelude::*; use toml; -pub use std::collections::HashMap; -pub use std::fmt; -pub use std::fs::File; -pub use std::io::{Write, Read}; +pub use std::{ + collections::HashMap, + fmt, + fs::{self, File}, + io::{self, Write, Read}, +}; + +mod error; +pub use error::SpecError; struct Line(Option); @@ -33,80 +40,182 @@ pub struct Config { #[derive(Debug, Deserialize, Eq, PartialEq)] pub struct Address{ pub name: String, + pub out_name: Option, pub x: String, pub y: String } +/// 使用 rust 编写的简易 spec 文件比较程序,支持 toml 格式的配置文件输入。 +/// 允许在控制台输出以及生成 diff 报告,输出报告格式为 markdown。 +/// toml 配置文件示例如下,多组软件对比放在其他 [[addresses]] 下即可 +/// ```toml +/// [[addresses]] +/// name = "fpaste" // 默认输出报告文件名 -specdiff-.md +/// out_name = "fpaste_34" // 自定义输出文件名,可以为空 +/// x = "https://src.fedoraproject.org/rpms/fpaste/raw/rawhide/f/fpaste.spec" +/// y = "https://src.fedoraproject.org/rpms/fpaste/raw/f34/f/fpaste.spec" +/// ``` #[derive(Parser)] +#[clap(version = "0.2.0", author = "Ke Lei ")] pub struct Cli { - path_config: String, + /// 一个 .toml 格式的配置文件的路径, 文件中需要有至少一个 [[addresses]] 配置项, 其中包括软件名字 name, 输出报告名称 out_name (可不填), 软件不同的spec文件地址 x 和 y (都是 String 类型) + pub path_config: String, + /// specdiff 输出的对比报告存放的路径, 默认为当前目录. + #[clap(short, long)] + pub report_out_path: Option, + /// 指定下载 spec 文件的保存目录, 默认为 /tmp/specdiff/download/ + #[clap(short, long)] + pub spec_save_path: Option, + /// 是否在控制台输出 diff 内容, 默认为 true + #[clap(short, long)] + pub terminal_out: Option, } impl Cli { - pub async fn get_address_list_from_cli() -> Result, Box> { + pub async fn get_address_list_from_cli() -> Result, SpecError> { let path = Cli::parse().path_config; - get_address_list(path) + get_address_list(&path) + } + + fn get_save_path() -> String { + match Cli::parse().spec_save_path { + Some(s) => s + "/", + None => "./".to_string(), + } } + + } -fn get_address_list(path: String) -> Result, Box> { + +fn get_address_list(path: &str) -> Result, SpecError> { let mut input = String::new(); - File::open(&path) + File::open(path) .and_then(|mut f| f.read_to_string(&mut input))?; let config:Config = toml::from_str(&input[..]).unwrap(); Ok(config.addresses.unwrap()) } - - - -pub async fn get_diff(name: String, specs: Vec) -> Result<(), Box> { - let diff = TextDiff::from_lines(&specs[0], &specs[1]); - - let path = name + "-diffreport.md"; - let file = File::create(&path[..]).expect("create failed"); +/// 获得 diff 内容,并通过参数决定是否输出相应内容到控制台 +pub async fn get_diff_from_address( + address: Address, + spec_save_path: &str, + out_terminal: &bool, + report_out_path: &str, + diff_ratio_list: &mut Vec, + writer: &mut dyn Write, +) -> Result<(), SpecError> { + let dt = Local::now(); + + let spec_list = match download_specs(&address, &spec_save_path).await { + Err(e) => { + panic!("Internal error: {:?}", e); + } + Ok(spec_list) => spec_list + }; + + + let mut report_name = report_out_path.to_string(); + report_name += "/"; + match address.out_name { + Some(name) => report_name = report_name + &name[..] + ".md", + None => { + let mut name = address.name.clone(); + name += "-specdiff-"; + name += &dt.format("%Y-%m-%d %H:%M:%S").to_string()[..]; + name += ".md"; + report_name += &name[..]; + } + } + let file = File::create(&report_name).expect("create failed"); let mut mdfile = Markdown::new(file); - + let diff = TextDiff::from_lines(&spec_list[0], &spec_list[1]); + let diff_ratio = diff.ratio(); + diff_ratio_list.push(diff_ratio); + + // let res: String = diff + // .grouped_ops(3) + // .iter() + // .map(|group| { + // group.iter().map(|op| { + // diff.iter_inline_changes(op) + // .map(|change| { + // format_line(change) + // }) + + // }).flatten() + // }) + + // .join("\n"); + for group in diff.grouped_ops(3).iter(){ for op in group { for change in diff.iter_inline_changes(op) { - let (sign, s) = match change.tag() { - ChangeTag::Delete => ("-", Style::new().red()), - ChangeTag::Insert => ("+", Style::new().green()), - ChangeTag::Equal => (" ", Style::new().dim()), - }; - print!( - "{}{} |{}", - style(Line(change.old_index())).dim(), - style(Line(change.new_index())).dim(), - s.apply_to(sign).bold(), - ); - - let mut line = format!("{:<4}|{}", Line(change.old_index()), sign); - // mdfile.write(&format!("{}{} | {}", change.old_index().unwrap(), change.new_index().unwrap(), sign)[..])?; - for (emphasized, value) in change.iter_strings_lossy() { - let st = value.clone(); - line.push_str(&format!("{}",st)); - if emphasized { - print!("{}", s.apply_to(value).underlined().on_black()); - } else { - print!("{}", s.apply_to(value)); - } - } + let line = format_line(change); + // let mut line = format!("{:<4}|{}", Line(change.old_index()), sign); mdfile.write(&line[..])?; - - if change.missing_newline() { - println!(); + if !*out_terminal{ + break; } + + writer.write_all(line.as_bytes())?; + writer.write_all(b"\n")?; } } - } + }; + println!("report written successfully in {}", report_name); + println!("diff-ratio for {} is: {}", address.name, diff_ratio); + + Ok(()) +} - println!("data written successfully in {}", path); - println!("diff ratio: {}", diff.ratio()); - Ok(()) +/// 下载一组 spec 文件,保存文件内容,并通过 Vec 返回 spec 内容 +/// 默认下载保存目录在 /tmp/specdiff/ +/// [TODO] 下载进度条 +pub async fn download_specs(address: &Address, spec_save_path: &str) -> Result, SpecError>{ + let f1 = reqwest::get(&address.x); + let f2 = reqwest::get(&address.y); + let (res1, res2) = try_join!(f1, f2)?; + let report_name = spec_save_path.to_string() + "/" + &address.name; + let mut file = File::create(&report_name)?; + + let (s1, s2) = (res1.text().await?, res2.text().await?); + file.write_all(s1.as_bytes())?; + file.write_all(b"\n")?; + file.write_all(s2.as_bytes())?; + Ok(vec![s1, s2]) + +} + +/// 格式化输出 diff 行 +pub fn format_line(change: similar::InlineChange) -> String { + let (sign, s) = match change.tag() { + ChangeTag::Delete => ("-", Style::new().red()), + ChangeTag::Insert => ("+", Style::new().green()), + ChangeTag::Equal => (" ", Style::new().dim()), + }; + + let mut line = format!( + "{}{} |{}", + style(Line(change.old_index())).dim(), + style(Line(change.new_index())).dim(), + s.apply_to(sign).bold(), + ); + + for (emphasized, value) in change.iter_strings_lossy() { + // let st = value.clone(); + // line.push_str(&format!("{}",st)); + let after = if emphasized { + format!("{}", s.apply_to(value).underlined().on_black()) + } else { + format!("{}", s.apply_to(value).underlined()) + }; + line += &after[..]; + } + + line } #[cfg(test)] @@ -114,14 +223,7 @@ mod tests { use super::*; - #[test] - fn toml_should_word() { - - } - #[test] - fn default_strategy_should_work() { - } } diff --git a/specdiff/src/main.rs b/specdiff/src/main.rs index e551b247..05b12380 100644 --- a/specdiff/src/main.rs +++ b/specdiff/src/main.rs @@ -2,21 +2,36 @@ use specdiff::*; #[tokio::main] -async fn main() -> Result<(), Box> { - +async fn main() -> Result<(), SpecError> { + let args = Cli::parse(); let addresses: Vec
= Cli::get_address_list_from_cli().await?; - let mut specs_list: HashMap> = HashMap::new(); + let (spec_save_path, out_terminal, report_out_path) = (args.spec_save_path, args.terminal_out, args.report_out_path); + let mut out = true; + let mut diff_ratio_list: Vec = Vec::new(); + let mut stdout = io::stdout(); + + let spec_path: String = match spec_save_path { + Some(path) => path, + None => "/tmp/specdiff/download/".to_string(), + }; + + let report_path: String = match report_out_path { + Some(path) => path, + None => ".".to_string(), + }; + + if let Some(false) = out_terminal { + out = false; + } + + fs::create_dir_all(&spec_path)?; + fs::create_dir_all(&report_path)?; for address in addresses { - let f1 = reqwest::get(address.x); - let f2 = reqwest::get(address.y); - let (res1, res2) = try_join!(f1, f2)?; - let body = vec![res1.text().await?, res2.text().await?]; - specs_list.insert(address.name, body); + get_diff_from_address(address, &spec_path, &out, &report_path, &mut diff_ratio_list, &mut stdout).await?; } - for (name, specs) in specs_list { - get_diff(name, specs).await?; - } + let avg_ratio:f32 = diff_ratio_list.iter().sum::() / diff_ratio_list.len() as f32; + println!("The avg_ratio is: {}", avg_ratio); Ok(()) } \ No newline at end of file diff --git a/specdiff/tests/integration_test.rs b/specdiff/tests/integration_test.rs index 7a05f9ac..b06b0feb 100644 --- a/specdiff/tests/integration_test.rs +++ b/specdiff/tests/integration_test.rs @@ -20,7 +20,13 @@ fn test_toml_should_work() { "#; let result = common::get_address_list(toml_str).unwrap(); - let expected = vec![Address{name : "fpaste_f34".to_string(), x : "https://src.fedoraproject.org/rpms/fpaste/raw/rawhide/f/fpaste.spec".to_string(), y : "https://src.fedoraproject.org/rpms/fpaste/raw/f34/f/fpaste.spec".to_string()}, - Address{name : "fpaste_epel8".to_string(), x :"https://src.fedoraproject.org/rpms/fpaste/raw/rawhide/f/fpaste.spec".to_string(), y : "https://src.fedoraproject.org/rpms/fpaste/raw/epel8/f/fpaste.spec".to_string()}]; + let expected = vec![Address{name : "fpaste_f34".to_string(), out_name: None, x : "https://src.fedoraproject.org/rpms/fpaste/raw/rawhide/f/fpaste.spec".to_string(), y : "https://src.fedoraproject.org/rpms/fpaste/raw/f34/f/fpaste.spec".to_string()}, + Address{name : "fpaste_epel8".to_string(), out_name: None, x :"https://src.fedoraproject.org/rpms/fpaste/raw/rawhide/f/fpaste.spec".to_string(), y : "https://src.fedoraproject.org/rpms/fpaste/raw/epel8/f/fpaste.spec".to_string()}]; assert_eq!(result, expected); } + + +#[test] +fn test_format_line_should_work() { + +} \ No newline at end of file -- Gitee