use tracing::instrument;
#[instrument(skip(client), fields(owner = %owner, repo = %repo, head_sha = %head_sha))]
pub async fn fetch_repo_instructions(
client: &octocrab::Octocrab,
owner: &str,
repo: &str,
head_sha: &str,
override_path: Option<&str>,
max_chars: usize,
) -> Option<String> {
let paths = if let Some(path) = override_path {
vec![path.to_string()]
} else {
vec![
"AGENTS.md".to_string(),
".github/instructions/pr-review.md".to_string(),
]
};
for path in paths {
match fetch_file_content(client, owner, repo, &path, head_sha).await {
Some(content) => {
if !content.is_empty() {
let stripped = strip_yaml_frontmatter(&content);
let truncated = truncate_to_chars(&stripped, max_chars);
tracing::debug!(
file = %path,
chars = truncated.len(),
"Fetched repo instructions"
);
return Some(truncated);
}
}
None => {
tracing::debug!(file = %path, "Instructions file not found or error fetching");
}
}
}
tracing::debug!("No instructions file found");
None
}
async fn fetch_file_content(
client: &octocrab::Octocrab,
owner: &str,
repo: &str,
filename: &str,
head_sha: &str,
) -> Option<String> {
match client
.repos(owner, repo)
.get_content()
.path(filename)
.r#ref(head_sha)
.send()
.await
{
Ok(content) => {
if let Some(item) = content.items.first() {
if let Some(decoded) = item.decoded_content() {
return Some(decoded);
}
tracing::debug!(
path = filename,
"failed to decode instructions file content"
);
return None;
}
None
}
Err(e) => {
tracing::debug!(error = %e, path = filename, "failed to fetch instructions file");
None
}
}
}
fn strip_yaml_frontmatter(content: &str) -> String {
let after_open = if let Some(rest) = content.strip_prefix("---\n") {
rest
} else if let Some(rest) = content.strip_prefix("---\r\n") {
rest
} else {
return content.to_string();
};
if let Some(end) = after_open.find("\n---\n") {
after_open[end + 5..].to_string()
} else if let Some(end) = after_open.find("\r\n---\r\n") {
after_open[end + 7..].to_string()
} else {
content.to_string()
}
}
fn truncate_to_chars(content: &str, max_chars: usize) -> String {
content.chars().take(max_chars).collect::<String>()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strip_yaml_frontmatter_with_frontmatter() {
let content = "---\ntitle: Test\nauthor: Me\n---\nActual content here";
let result = strip_yaml_frontmatter(content);
assert_eq!(result, "Actual content here");
}
#[test]
fn test_strip_yaml_frontmatter_without_frontmatter() {
let content = "Just plain content";
let result = strip_yaml_frontmatter(content);
assert_eq!(result, "Just plain content");
}
#[test]
fn test_strip_yaml_frontmatter_no_closing() {
let content = "---\ntitle: Test\nNo closing marker";
let result = strip_yaml_frontmatter(content);
assert_eq!(result, "---\ntitle: Test\nNo closing marker");
}
#[test]
fn test_truncate_to_chars() {
let content = "0123456789";
let result = truncate_to_chars(content, 5);
assert_eq!(result, "01234");
}
#[test]
fn test_truncate_to_chars_longer_than_max() {
let content = "short";
let result = truncate_to_chars(content, 100);
assert_eq!(result, "short");
}
#[test]
fn test_truncate_to_chars_unicode() {
let content = "hello 🌍 world";
let result = truncate_to_chars(content, 8);
assert_eq!(result, "hello 🌍 ");
}
}