-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgithub.rs
More file actions
135 lines (113 loc) · 3.47 KB
/
github.rs
File metadata and controls
135 lines (113 loc) · 3.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
use std::process::Command;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RepoMetadata {
pub name: String,
pub is_private: bool,
pub description: String,
}
pub fn list_repositories(org: &str, limit: usize) -> Result<Vec<RepoMetadata>, String> {
let jq = ".[] | [.name, (.isPrivate|tostring), (.description // \"\")] | @tsv";
let args = vec![
"repo".to_string(),
"list".to_string(),
org.to_string(),
"--limit".to_string(),
limit.to_string(),
"--json".to_string(),
"name,isPrivate,description".to_string(),
"--jq".to_string(),
jq.to_string(),
];
let result = run_gh(&args)?;
if !result.success {
return Err(format!(
"Failed to list repositories for '{org}': {}",
best_error(&result)
));
}
let repos: Vec<RepoMetadata> = result.stdout.lines().filter_map(parse_repo_line).collect();
if repos.is_empty() {
return Err(format!(
"No repositories returned for organization '{org}'. Check access with: gh auth status"
));
}
Ok(repos)
}
pub fn fetch_readme(org: &str, repo_name: &str) -> Result<Option<String>, String> {
let endpoint = format!("repos/{org}/{repo_name}/readme");
let args = vec![
"api".to_string(),
"-H".to_string(),
"Accept: application/vnd.github.raw+json".to_string(),
endpoint,
];
let result = run_gh(&args)?;
if result.success {
return Ok(Some(result.stdout));
}
let error_text = best_error(&result);
if error_text.contains("404") || error_text.contains("Not Found") {
return Ok(None);
}
Err(format!(
"Unable to fetch README for '{org}/{repo_name}': {error_text}"
))
}
#[derive(Debug)]
struct CommandResult {
success: bool,
stdout: String,
stderr: String,
}
fn run_gh(args: &[String]) -> Result<CommandResult, String> {
let output = Command::new("gh")
.args(args)
.output()
.map_err(|error| format!("Failed to execute gh command: {error}"))?;
Ok(CommandResult {
success: output.status.success(),
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
})
}
fn best_error(result: &CommandResult) -> String {
let stderr = result.stderr.trim();
if !stderr.is_empty() {
return stderr.to_string();
}
let stdout = result.stdout.trim();
if !stdout.is_empty() {
return stdout.to_string();
}
"unknown gh error".to_string()
}
fn parse_repo_line(line: &str) -> Option<RepoMetadata> {
let mut parts = line.splitn(3, '\t');
let name = parts.next()?.trim();
if name.is_empty() {
return None;
}
let is_private = parts.next().map(str::trim) == Some("true");
let description = parts.next().unwrap_or("").trim().to_string();
Some(RepoMetadata {
name: name.to_string(),
is_private,
description,
})
}
#[cfg(test)]
mod tests {
use super::parse_repo_line;
#[test]
fn parses_repo_tsv_line() {
let parsed = parse_repo_line("docs-sentry\tfalse\tREADME quality auditor")
.expect("line should parse");
assert_eq!(parsed.name, "docs-sentry");
assert!(!parsed.is_private);
assert_eq!(parsed.description, "README quality auditor");
}
#[test]
fn ignores_empty_repo_name() {
assert!(parse_repo_line("\tfalse\tmissing").is_none());
}
}