2026-04-20 15:13:26 +01:00
|
|
|
use std::collections::{BTreeMap, BTreeSet};
|
|
|
|
|
use std::env;
|
|
|
|
|
use std::fs;
|
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
struct AssetFile {
|
|
|
|
|
virtual_path: String,
|
|
|
|
|
disk_path: PathBuf,
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 00:32:12 +08:00
|
|
|
#[derive(Debug)]
|
|
|
|
|
struct TimelineFixturePage {
|
|
|
|
|
json: String,
|
|
|
|
|
end_cursor: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 15:13:26 +01:00
|
|
|
fn main() {
|
|
|
|
|
let manifest_dir =
|
|
|
|
|
PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("missing CARGO_MANIFEST_DIR"));
|
|
|
|
|
let asset_root = manifest_dir.join("src/asset");
|
2026-05-06 01:42:38 +08:00
|
|
|
let fixture_root = manifest_dir.join("fixtures/github");
|
|
|
|
|
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("missing OUT_DIR"));
|
|
|
|
|
let asset_out_file = out_dir.join("asset.rs");
|
|
|
|
|
let fixture_out_file = out_dir.join("github_fixtures.rs");
|
2026-04-20 15:13:26 +01:00
|
|
|
|
|
|
|
|
println!("cargo::rerun-if-changed={}", asset_root.display());
|
2026-05-06 01:42:38 +08:00
|
|
|
println!("cargo::rerun-if-changed={}", fixture_root.display());
|
2026-04-20 15:13:26 +01:00
|
|
|
|
|
|
|
|
let mut directory_entries = BTreeMap::<String, BTreeSet<String>>::new();
|
|
|
|
|
directory_entries
|
|
|
|
|
.entry(String::new())
|
|
|
|
|
.or_default()
|
|
|
|
|
.insert(String::from("asset"));
|
|
|
|
|
|
|
|
|
|
let mut asset_files = Vec::new();
|
|
|
|
|
collect_assets(
|
|
|
|
|
&asset_root,
|
|
|
|
|
"asset",
|
|
|
|
|
&mut directory_entries,
|
|
|
|
|
&mut asset_files,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let generated = render_assets(&asset_files, &directory_entries);
|
2026-05-06 01:42:38 +08:00
|
|
|
let fixture_module = render_github_fixtures(&fixture_root);
|
2026-04-20 15:13:26 +01:00
|
|
|
|
2026-05-06 01:42:38 +08:00
|
|
|
fs::write(asset_out_file, generated).expect("failed to write generated assets module");
|
|
|
|
|
fs::write(fixture_out_file, fixture_module)
|
|
|
|
|
.expect("failed to write generated github fixtures module");
|
2026-04-20 15:13:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn collect_assets(
|
|
|
|
|
disk_dir: &Path,
|
|
|
|
|
virtual_dir: &str,
|
|
|
|
|
directory_entries: &mut BTreeMap<String, BTreeSet<String>>,
|
|
|
|
|
asset_files: &mut Vec<AssetFile>,
|
|
|
|
|
) {
|
|
|
|
|
let mut entries = fs::read_dir(disk_dir)
|
|
|
|
|
.unwrap_or_else(|err| panic!("failed to read {}: {err}", disk_dir.display()))
|
|
|
|
|
.map(|entry| entry.expect("failed to read directory entry"))
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
|
|
entries.sort_by_key(|entry| entry.file_name());
|
|
|
|
|
|
|
|
|
|
for entry in entries {
|
|
|
|
|
let file_name = entry
|
|
|
|
|
.file_name()
|
|
|
|
|
.into_string()
|
|
|
|
|
.unwrap_or_else(|_| panic!("non-utf8 asset name in {}", disk_dir.display()));
|
|
|
|
|
let child_virtual_path = format!("{virtual_dir}/{file_name}");
|
|
|
|
|
let path = entry.path();
|
|
|
|
|
|
|
|
|
|
directory_entries
|
|
|
|
|
.entry(String::from(virtual_dir))
|
|
|
|
|
.or_default()
|
|
|
|
|
.insert(file_name);
|
|
|
|
|
|
|
|
|
|
if path.is_dir() {
|
|
|
|
|
collect_assets(&path, &child_virtual_path, directory_entries, asset_files);
|
|
|
|
|
} else if path.is_file() {
|
|
|
|
|
asset_files.push(AssetFile {
|
|
|
|
|
virtual_path: child_virtual_path,
|
|
|
|
|
disk_path: path,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn render_assets(
|
|
|
|
|
asset_files: &[AssetFile],
|
|
|
|
|
directory_entries: &BTreeMap<String, BTreeSet<String>>,
|
|
|
|
|
) -> String {
|
|
|
|
|
let mut output = String::new();
|
|
|
|
|
|
|
|
|
|
output.push_str(
|
|
|
|
|
"pub fn load_asset(path: &str) -> gpui::Result<Option<std::borrow::Cow<'static, [u8]>>> {\n",
|
|
|
|
|
);
|
|
|
|
|
output.push_str(" match path {\n");
|
|
|
|
|
for file in asset_files {
|
|
|
|
|
output.push_str(" ");
|
|
|
|
|
output.push_str(&string_literal(&file.virtual_path));
|
|
|
|
|
output.push_str(" => Ok(Some(std::borrow::Cow::Borrowed(include_bytes!(");
|
|
|
|
|
output.push_str(&string_literal(&file.disk_path.to_string_lossy()));
|
|
|
|
|
output.push_str(")))),\n");
|
|
|
|
|
}
|
|
|
|
|
output.push_str(" _ => Err(anyhow::anyhow!(\"asset not found: {path}\")),\n");
|
|
|
|
|
output.push_str(" }\n");
|
|
|
|
|
output.push_str("}\n\n");
|
|
|
|
|
|
|
|
|
|
output.push_str("pub fn list_assets(path: &str) -> gpui::Result<Vec<gpui::SharedString>> {\n");
|
|
|
|
|
output.push_str(" let normalized = path.trim_end_matches('/');\n");
|
|
|
|
|
output.push_str(" let normalized = if normalized == \".\" { \"\" } else { normalized };\n");
|
|
|
|
|
output.push_str(" let entries: &[&str] = match normalized {\n");
|
|
|
|
|
for (directory, entries) in directory_entries {
|
|
|
|
|
output.push_str(" ");
|
|
|
|
|
output.push_str(&string_literal(directory));
|
|
|
|
|
output.push_str(" => &[\n");
|
|
|
|
|
for entry in entries {
|
|
|
|
|
output.push_str(" ");
|
|
|
|
|
output.push_str(&string_literal(entry));
|
|
|
|
|
output.push_str(",\n");
|
|
|
|
|
}
|
|
|
|
|
output.push_str(" ],\n");
|
|
|
|
|
}
|
|
|
|
|
output.push_str(" _ => &[],\n");
|
|
|
|
|
output.push_str(" };\n");
|
|
|
|
|
output.push_str(" Ok(entries.iter().copied().map(gpui::SharedString::from).collect())\n");
|
|
|
|
|
output.push_str("}\n");
|
|
|
|
|
|
|
|
|
|
output
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 01:42:38 +08:00
|
|
|
fn render_github_fixtures(fixture_root: &Path) -> String {
|
|
|
|
|
let user_fetch = read_json_fixture(&fixture_root.join("user.fetch.json"));
|
|
|
|
|
let repo_list = read_json_fixture(&fixture_root.join("repo.list.json"));
|
2026-05-18 22:30:46 +08:00
|
|
|
let repo_file_content_root = fixture_root.join("repo.file_content");
|
2026-05-06 01:42:38 +08:00
|
|
|
|
|
|
|
|
let mut issue_fixtures = BTreeMap::<(String, u32), String>::new();
|
2026-05-11 00:32:12 +08:00
|
|
|
let mut pull_request_fixtures = BTreeMap::<String, String>::new();
|
2026-05-18 22:30:46 +08:00
|
|
|
let mut pull_request_file_tree_fixtures = BTreeMap::<String, String>::new();
|
|
|
|
|
let mut repo_file_content_fixtures =
|
|
|
|
|
BTreeMap::<(String, String, String, Option<String>), String>::new();
|
2026-05-11 00:32:12 +08:00
|
|
|
let mut pull_request_timeline_fixtures =
|
|
|
|
|
BTreeMap::<String, BTreeMap<u32, TimelineFixturePage>>::new();
|
2026-05-06 01:42:38 +08:00
|
|
|
let mut entries = fs::read_dir(fixture_root)
|
|
|
|
|
.unwrap_or_else(|err| panic!("failed to read {}: {err}", fixture_root.display()))
|
|
|
|
|
.map(|entry| entry.expect("failed to read github fixture entry"))
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
entries.sort_by_key(|entry| entry.file_name());
|
|
|
|
|
|
2026-05-18 22:30:46 +08:00
|
|
|
if repo_file_content_root.exists() {
|
|
|
|
|
collect_repo_file_content_fixtures(
|
|
|
|
|
&repo_file_content_root,
|
|
|
|
|
&repo_file_content_root,
|
|
|
|
|
&mut repo_file_content_fixtures,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 01:42:38 +08:00
|
|
|
for entry in entries {
|
|
|
|
|
let file_name = entry
|
|
|
|
|
.file_name()
|
|
|
|
|
.into_string()
|
|
|
|
|
.unwrap_or_else(|_| panic!("non-utf8 fixture name in {}", fixture_root.display()));
|
|
|
|
|
|
2026-05-11 00:32:12 +08:00
|
|
|
if let Some((filter, page)) = parse_issue_fixture_name(&file_name) {
|
|
|
|
|
let value = read_fixture_value(&entry.path());
|
|
|
|
|
issue_fixtures.insert((filter, page), read_issue_fixture(&value, &entry.path()));
|
2026-05-06 01:42:38 +08:00
|
|
|
continue;
|
2026-05-11 00:32:12 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(id) = parse_pull_request_fixture_name(&file_name) {
|
|
|
|
|
pull_request_fixtures.insert(id, read_json_fixture(&entry.path()));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-05-06 01:42:38 +08:00
|
|
|
|
2026-05-18 22:30:46 +08:00
|
|
|
if let Some(id) = parse_pull_request_file_tree_fixture_name(&file_name) {
|
|
|
|
|
let value = read_fixture_value(&entry.path());
|
|
|
|
|
pull_request_file_tree_fixtures
|
|
|
|
|
.insert(id, read_pull_request_file_tree_fixture(&value, &entry.path()));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 00:32:12 +08:00
|
|
|
if let Some((id, page)) = parse_pull_request_timeline_fixture_name(&file_name) {
|
|
|
|
|
let value = read_fixture_value(&entry.path());
|
|
|
|
|
pull_request_timeline_fixtures
|
|
|
|
|
.entry(id)
|
|
|
|
|
.or_default()
|
|
|
|
|
.insert(page, read_timeline_fixture(&value, &entry.path()));
|
|
|
|
|
}
|
2026-05-06 01:42:38 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut output = String::new();
|
|
|
|
|
|
|
|
|
|
output.push_str("pub fn user_fetch() -> &'static str {\n");
|
|
|
|
|
output.push_str(" ");
|
|
|
|
|
output.push_str(&string_literal(&user_fetch));
|
|
|
|
|
output.push_str("\n}\n\n");
|
|
|
|
|
|
|
|
|
|
output.push_str("pub fn repo_list() -> &'static str {\n");
|
|
|
|
|
output.push_str(" ");
|
|
|
|
|
output.push_str(&string_literal(&repo_list));
|
|
|
|
|
output.push_str("\n}\n\n");
|
|
|
|
|
|
2026-05-08 02:23:28 +08:00
|
|
|
output.push_str(
|
|
|
|
|
"pub fn issues_pull_requests(filter: &str, page: u32) -> Option<&'static str> {\n",
|
|
|
|
|
);
|
2026-05-06 01:42:38 +08:00
|
|
|
output.push_str(" match (filter, page) {\n");
|
|
|
|
|
for ((filter, page), json) in issue_fixtures {
|
|
|
|
|
output.push_str(" (");
|
|
|
|
|
output.push_str(&string_literal(&filter));
|
|
|
|
|
output.push_str(", ");
|
|
|
|
|
output.push_str(&page.to_string());
|
|
|
|
|
output.push_str(") => Some(");
|
|
|
|
|
output.push_str(&string_literal(&json));
|
|
|
|
|
output.push_str("),\n");
|
|
|
|
|
}
|
|
|
|
|
output.push_str(" _ => None,\n");
|
|
|
|
|
output.push_str(" }\n");
|
|
|
|
|
output.push_str("}\n");
|
|
|
|
|
|
2026-05-18 22:30:46 +08:00
|
|
|
output.push_str("\n");
|
|
|
|
|
output.push_str(
|
|
|
|
|
"pub fn repo_file_content(owner: &str, repo: &str, path: &str, reff: Option<&str>) -> Option<&'static str> {\n",
|
|
|
|
|
);
|
|
|
|
|
output.push_str(" match (owner, repo, path, reff) {\n");
|
|
|
|
|
for ((owner, repo, path, reff), content) in repo_file_content_fixtures {
|
|
|
|
|
output.push_str(" (");
|
|
|
|
|
output.push_str(&string_literal(&owner));
|
|
|
|
|
output.push_str(", ");
|
|
|
|
|
output.push_str(&string_literal(&repo));
|
|
|
|
|
output.push_str(", ");
|
|
|
|
|
output.push_str(&string_literal(&path));
|
|
|
|
|
output.push_str(", ");
|
|
|
|
|
match reff {
|
|
|
|
|
| Some(reff) => {
|
|
|
|
|
output.push_str("Some(");
|
|
|
|
|
output.push_str(&string_literal(&reff));
|
|
|
|
|
output.push(')');
|
|
|
|
|
}
|
|
|
|
|
| None => output.push_str("None"),
|
|
|
|
|
}
|
|
|
|
|
output.push_str(") => Some(");
|
|
|
|
|
output.push_str(&string_literal(&content));
|
|
|
|
|
output.push_str("),\n");
|
|
|
|
|
}
|
|
|
|
|
output.push_str(" _ => None,\n");
|
|
|
|
|
output.push_str(" }\n");
|
|
|
|
|
output.push_str("}\n");
|
|
|
|
|
|
2026-05-11 00:32:12 +08:00
|
|
|
output.push_str("\n");
|
|
|
|
|
output.push_str("pub fn issues_pull_request(id: &str) -> Option<&'static str> {\n");
|
|
|
|
|
output.push_str(" match id {\n");
|
|
|
|
|
for (id, json) in pull_request_fixtures {
|
|
|
|
|
output.push_str(" ");
|
|
|
|
|
output.push_str(&string_literal(&id));
|
|
|
|
|
output.push_str(" => Some(");
|
|
|
|
|
output.push_str(&string_literal(&json));
|
|
|
|
|
output.push_str("),\n");
|
|
|
|
|
}
|
|
|
|
|
output.push_str(" _ => None,\n");
|
|
|
|
|
output.push_str(" }\n");
|
|
|
|
|
output.push_str("}\n");
|
|
|
|
|
|
2026-05-18 22:30:46 +08:00
|
|
|
output.push_str("\n");
|
|
|
|
|
output.push_str("pub fn issues_pull_request_file_tree(id: &str) -> Option<&'static str> {\n");
|
|
|
|
|
output.push_str(" match id {\n");
|
|
|
|
|
for (id, json) in pull_request_file_tree_fixtures {
|
|
|
|
|
output.push_str(" ");
|
|
|
|
|
output.push_str(&string_literal(&id));
|
|
|
|
|
output.push_str(" => Some(");
|
|
|
|
|
output.push_str(&string_literal(&json));
|
|
|
|
|
output.push_str("),\n");
|
|
|
|
|
}
|
|
|
|
|
output.push_str(" _ => None,\n");
|
|
|
|
|
output.push_str(" }\n");
|
|
|
|
|
output.push_str("}\n");
|
|
|
|
|
|
2026-05-11 00:32:12 +08:00
|
|
|
output.push_str("\n");
|
|
|
|
|
output.push_str("pub fn issues_pull_request_timeline(id: &str, after: Option<&str>) -> Option<&'static str> {\n");
|
|
|
|
|
output.push_str(" match (id, after) {\n");
|
|
|
|
|
for (id, pages) in pull_request_timeline_fixtures {
|
|
|
|
|
let mut previous_page = 0;
|
|
|
|
|
let mut previous_end_cursor = None;
|
|
|
|
|
|
|
|
|
|
for (page, fixture) in pages {
|
|
|
|
|
if page != previous_page + 1 {
|
|
|
|
|
panic!("missing pull request timeline fixture page {page} for {id}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
output.push_str(" (");
|
|
|
|
|
output.push_str(&string_literal(&id));
|
|
|
|
|
output.push_str(", ");
|
|
|
|
|
match previous_end_cursor.as_deref() {
|
2026-05-13 20:02:26 +08:00
|
|
|
| Some(after) => output.push_str(&format!("Some({})", string_literal(after))),
|
|
|
|
|
| None => output.push_str("None"),
|
2026-05-11 00:32:12 +08:00
|
|
|
}
|
|
|
|
|
output.push_str(") => Some(");
|
|
|
|
|
output.push_str(&string_literal(&fixture.json));
|
|
|
|
|
output.push_str("),\n");
|
|
|
|
|
|
|
|
|
|
previous_page = page;
|
|
|
|
|
previous_end_cursor = fixture.end_cursor.clone();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
output.push_str(" _ => None,\n");
|
|
|
|
|
output.push_str(" }\n");
|
|
|
|
|
output.push_str("}\n");
|
|
|
|
|
|
2026-05-06 01:42:38 +08:00
|
|
|
output
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 00:32:12 +08:00
|
|
|
fn read_fixture_value(path: &Path) -> serde_json::Value {
|
2026-05-06 01:42:38 +08:00
|
|
|
let raw = fs::read_to_string(path)
|
|
|
|
|
.unwrap_or_else(|err| panic!("failed to read fixture {}: {err}", path.display()));
|
2026-05-11 00:32:12 +08:00
|
|
|
serde_json::from_str(&raw)
|
|
|
|
|
.unwrap_or_else(|err| panic!("invalid json fixture {}: {err}", path.display()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn read_json_fixture(path: &Path) -> String {
|
|
|
|
|
let value = read_fixture_value(path);
|
2026-05-06 01:42:38 +08:00
|
|
|
serde_json::to_string(&value)
|
|
|
|
|
.unwrap_or_else(|err| panic!("failed to serialize fixture {}: {err}", path.display()))
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 00:32:12 +08:00
|
|
|
fn read_issue_fixture(value: &serde_json::Value, path: &Path) -> String {
|
2026-05-08 02:23:28 +08:00
|
|
|
let issues = value
|
|
|
|
|
.as_array()
|
|
|
|
|
.unwrap_or_else(|| panic!("issue fixture {} must be a json array", path.display()));
|
|
|
|
|
|
|
|
|
|
let items = issues.iter().map(map_issue_fixture).collect::<Vec<_>>();
|
|
|
|
|
let start_cursor = issues.first().and_then(issue_fixture_cursor);
|
|
|
|
|
let end_cursor = issues.last().and_then(issue_fixture_cursor);
|
|
|
|
|
|
|
|
|
|
serde_json::to_string(&serde_json::json!({
|
|
|
|
|
"items": items,
|
|
|
|
|
"start_cursor": start_cursor,
|
|
|
|
|
"end_cursor": end_cursor,
|
|
|
|
|
}))
|
|
|
|
|
.unwrap_or_else(|err| {
|
|
|
|
|
panic!(
|
|
|
|
|
"failed to serialize mapped issue fixture {}: {err}",
|
|
|
|
|
path.display()
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 00:32:12 +08:00
|
|
|
fn read_timeline_fixture(value: &serde_json::Value, path: &Path) -> TimelineFixturePage {
|
|
|
|
|
let page_info = value
|
|
|
|
|
.get("node")
|
|
|
|
|
.and_then(|node| node.get("timelineItems"))
|
|
|
|
|
.and_then(|timeline| timeline.get("pageInfo"))
|
|
|
|
|
.unwrap_or_else(|| {
|
|
|
|
|
panic!(
|
|
|
|
|
"timeline fixture {} must include node.timelineItems.pageInfo",
|
|
|
|
|
path.display()
|
|
|
|
|
)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if !matches!(
|
|
|
|
|
value
|
|
|
|
|
.get("node")
|
|
|
|
|
.and_then(|node| node.get("timelineItems"))
|
|
|
|
|
.and_then(|timeline| timeline.get("nodes")),
|
|
|
|
|
Some(serde_json::Value::Array(_))
|
|
|
|
|
) {
|
|
|
|
|
panic!(
|
|
|
|
|
"timeline fixture {} must include node.timelineItems.nodes",
|
|
|
|
|
path.display()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let end_cursor = page_info
|
|
|
|
|
.get("endCursor")
|
|
|
|
|
.and_then(serde_json::Value::as_str)
|
|
|
|
|
.map(str::to_owned);
|
|
|
|
|
|
|
|
|
|
let json = serde_json::to_string(value).unwrap_or_else(|err| {
|
|
|
|
|
panic!(
|
|
|
|
|
"failed to serialize mapped timeline fixture {}: {err}",
|
|
|
|
|
path.display()
|
|
|
|
|
)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
TimelineFixturePage { json, end_cursor }
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 22:30:46 +08:00
|
|
|
fn read_pull_request_file_tree_fixture(value: &serde_json::Value, path: &Path) -> String {
|
|
|
|
|
if !matches!(
|
|
|
|
|
value
|
|
|
|
|
.get("node")
|
|
|
|
|
.and_then(|node| node.get("files"))
|
|
|
|
|
.and_then(|files| files.get("edges")),
|
|
|
|
|
Some(serde_json::Value::Array(_))
|
|
|
|
|
) {
|
|
|
|
|
panic!(
|
|
|
|
|
"pull request file tree fixture {} must include node.files.edges",
|
|
|
|
|
path.display()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serde_json::to_string(value).unwrap_or_else(|err| {
|
|
|
|
|
panic!(
|
|
|
|
|
"failed to serialize pull request file tree fixture {}: {err}",
|
|
|
|
|
path.display()
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-08 02:23:28 +08:00
|
|
|
fn map_issue_fixture(issue: &serde_json::Value) -> serde_json::Value {
|
|
|
|
|
serde_json::json!({
|
2026-05-11 00:32:12 +08:00
|
|
|
"id": issue_fixture_graphql_id(issue),
|
2026-05-08 02:23:28 +08:00
|
|
|
"title": required_string(issue, &["title"]),
|
|
|
|
|
"state": issue_fixture_state(issue),
|
|
|
|
|
"is_draft": issue.get("draft").and_then(serde_json::Value::as_bool).unwrap_or(false),
|
|
|
|
|
"repo_slug": required_string(issue, &["repository", "full_name"]),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn issue_fixture_state(issue: &serde_json::Value) -> &'static str {
|
|
|
|
|
if issue
|
|
|
|
|
.get("pull_request")
|
|
|
|
|
.and_then(|pull_request| pull_request.get("merged_at"))
|
|
|
|
|
.and_then(serde_json::Value::as_str)
|
|
|
|
|
.is_some()
|
|
|
|
|
{
|
2026-05-11 00:32:12 +08:00
|
|
|
return "MERGED";
|
2026-05-08 02:23:28 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match required_string(issue, &["state"]) {
|
2026-05-13 20:02:26 +08:00
|
|
|
| "open" => "OPEN",
|
|
|
|
|
| "closed" => "CLOSED",
|
|
|
|
|
| state => panic!("unsupported pull request state in fixture: {state}"),
|
2026-05-08 02:23:28 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 00:32:12 +08:00
|
|
|
fn issue_fixture_graphql_id<'a>(issue: &'a serde_json::Value) -> &'a str {
|
|
|
|
|
required_string(issue, &["node_id"])
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-08 02:23:28 +08:00
|
|
|
fn issue_fixture_cursor(issue: &serde_json::Value) -> Option<String> {
|
2026-05-11 00:32:12 +08:00
|
|
|
Some(issue_fixture_graphql_id(issue).to_owned())
|
2026-05-08 02:23:28 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn required_string<'a>(value: &'a serde_json::Value, path: &[&str]) -> &'a str {
|
|
|
|
|
let mut current = value;
|
|
|
|
|
|
|
|
|
|
for key in path {
|
|
|
|
|
current = current
|
|
|
|
|
.get(*key)
|
|
|
|
|
.unwrap_or_else(|| panic!("missing key {} in fixture", path.join(".")));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
current
|
|
|
|
|
.as_str()
|
|
|
|
|
.unwrap_or_else(|| panic!("expected string at {} in fixture", path.join(".")))
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 22:30:46 +08:00
|
|
|
fn collect_repo_file_content_fixtures(
|
|
|
|
|
root: &Path,
|
|
|
|
|
dir: &Path,
|
|
|
|
|
fixtures: &mut BTreeMap<(String, String, String, Option<String>), String>,
|
|
|
|
|
) {
|
|
|
|
|
let mut entries = fs::read_dir(dir)
|
|
|
|
|
.unwrap_or_else(|err| panic!("failed to read {}: {err}", dir.display()))
|
|
|
|
|
.map(|entry| entry.expect("failed to read repo file content fixture entry"))
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
entries.sort_by_key(|entry| entry.file_name());
|
|
|
|
|
|
|
|
|
|
for entry in entries {
|
|
|
|
|
let path = entry.path();
|
|
|
|
|
if path.is_dir() {
|
|
|
|
|
collect_repo_file_content_fixtures(root, &path, fixtures);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !path.is_file() {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let relative = path.strip_prefix(root).unwrap_or_else(|err| {
|
|
|
|
|
panic!(
|
|
|
|
|
"failed to compute repo file content fixture path {}: {err}",
|
|
|
|
|
path.display()
|
|
|
|
|
)
|
|
|
|
|
});
|
|
|
|
|
let parts = relative
|
|
|
|
|
.components()
|
|
|
|
|
.map(|component| component.as_os_str().to_string_lossy().into_owned())
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
|
|
if parts.len() < 4 {
|
|
|
|
|
panic!(
|
|
|
|
|
"repo file content fixture {} must be nested as owner/repo/ref/path",
|
|
|
|
|
path.display()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let owner = parts[0].clone();
|
|
|
|
|
let repo = parts[1].clone();
|
|
|
|
|
let reff = match parts[2].as_str() {
|
|
|
|
|
| "@default" => None,
|
|
|
|
|
| value => Some(value.to_owned()),
|
|
|
|
|
};
|
|
|
|
|
let virtual_path = parts[3..].join("/");
|
|
|
|
|
let content = fs::read_to_string(&path).unwrap_or_else(|err| {
|
|
|
|
|
panic!(
|
|
|
|
|
"failed to read repo file content fixture {}: {err}",
|
|
|
|
|
path.display()
|
|
|
|
|
)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if fixtures
|
|
|
|
|
.insert((owner, repo, virtual_path, reff), content)
|
|
|
|
|
.is_some()
|
|
|
|
|
{
|
|
|
|
|
panic!("duplicate repo file content fixture for {}", path.display());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 01:42:38 +08:00
|
|
|
fn parse_issue_fixture_name(file_name: &str) -> Option<(String, u32)> {
|
|
|
|
|
let name = file_name.strip_suffix(".json")?;
|
|
|
|
|
let rest = name.strip_prefix("issues.pull_requests.")?;
|
|
|
|
|
let (filter, page) = rest.rsplit_once(".page")?;
|
|
|
|
|
let page = page.parse::<u32>().ok()?;
|
|
|
|
|
Some((filter.to_owned(), page))
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 00:32:12 +08:00
|
|
|
fn parse_pull_request_fixture_name(file_name: &str) -> Option<String> {
|
|
|
|
|
let name = file_name.strip_suffix(".json")?;
|
|
|
|
|
name.strip_prefix("issues.pull_request.").map(str::to_owned)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_pull_request_timeline_fixture_name(file_name: &str) -> Option<(String, u32)> {
|
|
|
|
|
let name = file_name.strip_suffix(".json")?;
|
|
|
|
|
let rest = name.strip_prefix("issues.pull_request_timeline.")?;
|
|
|
|
|
let (id, page) = rest.rsplit_once(".page")?;
|
|
|
|
|
let page = page.parse::<u32>().ok()?;
|
|
|
|
|
Some((id.to_owned(), page))
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 22:30:46 +08:00
|
|
|
fn parse_pull_request_file_tree_fixture_name(file_name: &str) -> Option<String> {
|
|
|
|
|
let name = file_name.strip_suffix(".json")?;
|
|
|
|
|
name.strip_prefix("issues.pull_request_file_tree.")
|
|
|
|
|
.map(str::to_owned)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 15:13:26 +01:00
|
|
|
fn string_literal(value: &str) -> String {
|
|
|
|
|
format!("{value:?}")
|
|
|
|
|
}
|