From 2b54f5c51ff9a26d4077037631ed39d62ed2b3fb Mon Sep 17 00:00:00 2001 From: Joel Klinghed Date: Thu, 12 Jun 2025 09:11:18 +0200 Subject: Initial support for translation reviews --- server/common/src/git.rs | 61 +++++++++++++++++++++++++++-- server/common/src/grit.rs | 98 +++++++++++++++++++++++++++++++++++++---------- 2 files changed, 136 insertions(+), 23 deletions(-) (limited to 'server/common/src') diff --git a/server/common/src/git.rs b/server/common/src/git.rs index 8fe7863..e396d8a 100644 --- a/server/common/src/git.rs +++ b/server/common/src/git.rs @@ -4,6 +4,7 @@ use futures::future::TryFutureExt; use pathdiff::diff_paths; use std::collections::HashMap; use std::fmt; +use std::io::{self, Cursor, Read}; use std::path::{Path, PathBuf}; use std::process::Stdio; use tokio::fs; @@ -78,6 +79,24 @@ pub struct TreeEntry { pub path: String, } +pub struct GitFile { + cursor: Cursor>, +} + +impl GitFile { + pub fn new(data: Vec) -> Self { + GitFile { + cursor: Cursor::new(data), + } + } +} + +impl Read for GitFile { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.cursor.read(buf) + } +} + fn io_err(action: &str, e: std::io::Error) -> Error { Error::new(format!("{action}: {e}")) } @@ -161,6 +180,8 @@ impl RepoData { cmd.arg("--porcelain"); // This option disables this automatic tag following. cmd.arg("--no-tags"); + // Write out refs even if they didn't change + cmd.arg("--verbose"); cmd.arg("origin"); // <+ force update>: cmd.arg(format!("+{branch}:{branch}")); @@ -430,6 +451,23 @@ impl RepoData { self.output(&mut cmd).map_ok(parse_tree_entries).await } + async fn cat_file( + &self, + repo: &Repository, + object_type: ObjectType, + object_name: &str, + ) -> Result { + let mut cmd = self.git_cmd(repo); + cmd.arg("cat-file") + .arg(match object_type { + ObjectType::BLOB => "blob", + ObjectType::COMMIT => "commit", + ObjectType::TREE => "tree", + }) + .arg(object_name); + self.raw_output(&mut cmd).map_ok(GitFile::new).await + } + async fn get_log_format( &self, repo: &Repository, @@ -516,6 +554,14 @@ impl RepoData { } async fn output(&self, cmd: &mut Command) -> Result { + match self.raw_output(cmd).await { + Ok(bytes) => String::from_utf8(bytes) + .map_err(|e| Error::new(format!("git command had invalid output: {e}"))), + Err(e) => Err(e), + } + } + + async fn raw_output(&self, cmd: &mut Command) -> Result, Error> { cmd.stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); @@ -530,9 +576,7 @@ impl RepoData { .await?; if output.status.success() { - let output_utf8 = String::from_utf8(output.stdout) - .map_err(|e| Error::new(format!("git command had invalid output: {e}")))?; - Ok(output_utf8) + Ok(output.stdout) } else { Err(Error::new(format!( "git command failed with exitcode: {}\n{:?}\n{}", @@ -693,4 +737,15 @@ impl Repository { data.ls_tree(self, commit.as_str(), recursive).await } + + pub async fn cat_file( + &self, + object_type: ObjectType, + object_name: impl Into, + ) -> Result { + let object_name = object_name.into(); + let data = self.lock.read().await; + + data.cat_file(self, object_type, object_name.as_str()).await + } } diff --git a/server/common/src/grit.rs b/server/common/src/grit.rs index ee96500..9d01dac 100644 --- a/server/common/src/grit.rs +++ b/server/common/src/grit.rs @@ -4,7 +4,7 @@ use anyhow::Error; use std::collections::VecDeque; use std::fs; use std::io::{BufReader, Read}; -use std::path::Path; +use std::path::{Path, PathBuf}; use tokio::task::spawn_blocking; use xml::attribute::OwnedAttribute; use xml::reader::{EventReader, ParserConfig, XmlEvent}; @@ -1018,9 +1018,16 @@ fn parse_grit_part_element( pub async fn parse_grit(path: impl AsRef) -> anyhow::Result { let path = path.as_ref().to_path_buf(); + parse_grit_with_opener(move || Ok(BufReader::new(fs::File::open(path)?))).await +} + +pub async fn parse_grit_with_opener(opener: F) -> anyhow::Result +where + F: FnOnce() -> anyhow::Result> + Send + 'static, + R: Read, +{ spawn_blocking(move || { - let file = fs::File::open(path)?; - let reader = BufReader::new(file); + let reader = opener()?; let mut ereader = ParserConfig::new() .ignore_comments(true) .whitespace_to_characters(true) @@ -1064,9 +1071,16 @@ pub async fn parse_grit(path: impl AsRef) -> anyhow::Result { pub async fn parse_grit_part(path: impl AsRef) -> anyhow::Result { let path = path.as_ref().to_path_buf(); + parse_grit_part_with_opener(|| Ok(BufReader::new(fs::File::open(path)?))).await +} + +pub async fn parse_grit_part_with_opener(opener: F) -> anyhow::Result +where + F: FnOnce() -> anyhow::Result> + Send + 'static, + R: Read, +{ spawn_blocking(move || { - let file = fs::File::open(path)?; - let reader = BufReader::new(file); + let reader = opener()?; let mut ereader = ParserConfig::new() .ignore_comments(true) .whitespace_to_characters(true) @@ -1121,20 +1135,20 @@ fn if_message_to_if_message_part(messages: Vec) -> Vec .collect() } -async fn maybe_expand_message(message: &mut IfMessagePart, basepath: &Path) -> anyhow::Result<()> { +async fn maybe_expand_message(message: &mut IfMessagePart, opener: &F) -> anyhow::Result<()> +where + F: Fn(&str) -> anyhow::Result> + Clone + Send + 'static, + R: Read, +{ match message { IfMessagePart::Message(_) => Ok(()), IfMessagePart::Part { file, ref mut messages, } => { - let file_path = Path::new(file.as_str()); - let part_path = if let Some(parent) = basepath.parent() { - parent.join(file_path) - } else { - file_path.to_path_buf() - }; - let grit_part = parse_grit_part(part_path).await?; + let file = file.to_string(); + let opener = opener.clone(); + let grit_part = parse_grit_part_with_opener(move || opener(file.as_str())).await?; *messages = if_message_to_if_message_part(grit_part.messages); Ok(()) } @@ -1142,23 +1156,60 @@ async fn maybe_expand_message(message: &mut IfMessagePart, basepath: &Path) -> a expr: _, ref mut message, } => { - Box::pin(expand_messages(message, basepath)).await?; + Box::pin(expand_messages(message, opener)).await?; Ok(()) } } } -async fn expand_messages(messages: &mut Vec, basepath: &Path) -> anyhow::Result<()> { +async fn expand_messages(messages: &mut Vec, opener: &F) -> anyhow::Result<()> +where + F: Fn(&str) -> anyhow::Result> + Clone + Send + 'static, + R: Read, +{ for message in messages { - maybe_expand_message(message, basepath).await?; + maybe_expand_message(message, opener).await?; } Ok(()) } pub async fn parse_grit_with_parts(path: impl AsRef) -> anyhow::Result { let path = path.as_ref(); - let mut grit = parse_grit(path).await?; - expand_messages(&mut grit.release.messages.messages, path).await?; + if let Some(basepath) = path.parent() { + let basepath = basepath.to_path_buf(); + parse_grit_with_parts_and_resolver(path, move |x| basepath.join(x)).await + } else { + parse_grit_with_parts_and_resolver(path, |x| PathBuf::from(x)).await + } +} + +pub async fn parse_grit_with_parts_and_resolver( + path: impl AsRef, + path_resolver: F, +) -> anyhow::Result +where + F: Fn(&str) -> PathBuf + Send + Clone + 'static, +{ + let path = path.as_ref().to_path_buf(); + let grit_opener = || Ok(BufReader::new(fs::File::open(path)?)); + let part_opener = move |x: &str| { + let part_path = path_resolver(x); + Ok(BufReader::new(fs::File::open(part_path)?)) + }; + parse_grit_with_parts_and_opener(grit_opener, part_opener).await +} + +pub async fn parse_grit_with_parts_and_opener( + grit_opener: F, + part_opener: G, +) -> anyhow::Result +where + F: FnOnce() -> anyhow::Result> + Send + 'static, + G: Fn(&str) -> anyhow::Result> + Clone + Send + 'static, + R: Read, +{ + let mut grit = parse_grit_with_opener(grit_opener).await?; + expand_messages(&mut grit.release.messages.messages, &part_opener).await?; Ok(grit) } @@ -1513,9 +1564,16 @@ fn parse_xliff_element( pub async fn parse_xlf(path: impl AsRef) -> anyhow::Result { let path = path.as_ref().to_path_buf(); + parse_xlf_with_opener(|| Ok(BufReader::new(fs::File::open(path)?))).await +} + +pub async fn parse_xlf_with_opener(opener: F) -> anyhow::Result +where + F: FnOnce() -> anyhow::Result> + Send + 'static, + R: Read, +{ spawn_blocking(move || { - let file = fs::File::open(path)?; - let reader = BufReader::new(file); + let reader = opener()?; let mut ereader = ParserConfig::new() .ignore_comments(true) .whitespace_to_characters(true) -- cgit v1.2.3-70-g09d2