use anyhow::{bail, Context, Result};
use console::style;
use serde_json::json;
use std::path::Path;
pub fn get_authenticated_user(token: &str) -> Result<String> {
let text = ureq::get("https://api.github.com/user")
.header("Authorization", &format!("Bearer {}", token))
.header("Accept", "application/vnd.github+json")
.header("User-Agent", "fledge-cli")
.call()
.context("GitHub API request failed")?
.body_mut()
.read_to_string()
.context("reading GitHub user response")?;
let response: serde_json::Value =
serde_json::from_str(&text).context("parsing GitHub user response")?;
response["login"]
.as_str()
.map(|s| s.to_string())
.ok_or_else(|| anyhow::anyhow!("Could not determine GitHub username"))
}
pub fn check_repo_exists(owner: &str, repo: &str, token: &str) -> Result<bool> {
let url = format!("https://api.github.com/repos/{}/{}", owner, repo);
let result = ureq::get(&url)
.header("Authorization", &format!("Bearer {}", token))
.header("Accept", "application/vnd.github+json")
.header("User-Agent", "fledge-cli")
.call();
match result {
Ok(_) => Ok(true),
Err(ureq::Error::StatusCode(404)) => Ok(false),
Err(e) => Err(anyhow::anyhow!("GitHub API error: {}", e)),
}
}
pub fn create_github_repo(
name: &str,
description: &str,
private: bool,
org: Option<&str>,
token: &str,
) -> Result<()> {
let url = match org {
Some(o) => format!("https://api.github.com/orgs/{}/repos", o),
None => "https://api.github.com/user/repos".to_string(),
};
let body = json!({
"name": name,
"description": description,
"private": private,
"auto_init": false,
});
let json_body = serde_json::to_string(&body).context("serializing request body")?;
let result = ureq::post(&url)
.header("Authorization", &format!("Bearer {}", token))
.header("Accept", "application/vnd.github+json")
.header("User-Agent", "fledge-cli")
.header("Content-Type", "application/json")
.send(json_body.as_bytes());
match result {
Ok(_) => Ok(()),
Err(ureq::Error::StatusCode(422)) => {
bail!("Repository '{}' already exists or name is invalid", name)
}
Err(ureq::Error::StatusCode(403)) => {
bail!("Permission denied. Check your token has 'repo' scope.")
}
Err(e) => bail!("Failed to create repository: {}", e),
}
}
pub fn set_repo_topic(owner: &str, repo: &str, topic: &str, token: &str) -> Result<()> {
let url = format!("https://api.github.com/repos/{}/{}/topics", owner, repo);
let text = ureq::get(&url)
.header("Authorization", &format!("Bearer {}", token))
.header("Accept", "application/vnd.github+json")
.header("User-Agent", "fledge-cli")
.call()
.context("fetching repo topics")?
.body_mut()
.read_to_string()
.context("reading topics response")?;
let existing: serde_json::Value =
serde_json::from_str(&text).context("parsing topics response")?;
let mut topics: Vec<String> = existing["names"]
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
if !topics.iter().any(|t| t == topic) {
topics.push(topic.to_string());
}
let body = json!({ "names": topics });
let json_body = serde_json::to_string(&body).context("serializing topics")?;
ureq::put(&url)
.header("Authorization", &format!("Bearer {}", token))
.header("Accept", "application/vnd.github+json")
.header("User-Agent", "fledge-cli")
.header("Content-Type", "application/json")
.send(json_body.as_bytes())
.context("setting repo topics")?;
Ok(())
}
pub fn push_directory(path: &Path, owner: &str, repo: &str, token: &str) -> Result<()> {
let git_dir = path.join(".git");
let needs_init = !git_dir.exists();
if needs_init {
run_git(path, &["init"])?;
run_git(path, &["checkout", "-b", "main"])?;
}
let remote_url = format!("https://github.com/{}/{}.git", owner, repo);
let has_remote = std::process::Command::new("git")
.args(["remote", "get-url", "origin"])
.current_dir(path)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
if has_remote {
run_git(path, &["remote", "set-url", "origin", &remote_url])?;
} else {
run_git(path, &["remote", "add", "origin", &remote_url])?;
}
run_git(path, &["add", "-A"])?;
let has_changes = !std::process::Command::new("git")
.args(["diff", "--cached", "--quiet"])
.current_dir(path)
.status()
.map(|s| s.success())
.unwrap_or(false);
if has_changes {
run_git(path, &["commit", "-m", "Publish fledge template"])?;
}
use base64::Engine;
let credentials = format!("x-access-token:{}", token);
let encoded = base64::engine::general_purpose::STANDARD.encode(&credentials);
let header_value = format!("Authorization: Basic {}", encoded);
let existing: usize = std::env::var("GIT_CONFIG_COUNT")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(0);
println!(
"{} Force-pushing to {}/{}...",
style("*").cyan().bold(),
owner,
repo
);
let status = std::process::Command::new("git")
.args(["push", "-u", "origin", "main", "--force"])
.current_dir(path)
.env("GIT_CONFIG_COUNT", (existing + 1).to_string())
.env(format!("GIT_CONFIG_KEY_{existing}"), "http.extraheader")
.env(format!("GIT_CONFIG_VALUE_{existing}"), &header_value)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.status()
.context("running git push")?;
if !status.success() {
bail!(
"Failed to push to {}/{}. Check your token has 'repo' scope.",
owner,
repo
);
}
if needs_init {
let clean_url = format!("https://github.com/{}/{}.git", owner, repo);
let _ = run_git(path, &["remote", "set-url", "origin", &clean_url]);
}
Ok(())
}
pub fn run_git(dir: &Path, args: &[&str]) -> Result<()> {
let status = std::process::Command::new("git")
.args(args)
.current_dir(dir)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.with_context(|| format!("running git {}", args.join(" ")))?;
if !status.success() {
bail!("git {} failed", args.join(" "));
}
Ok(())
}
#[cfg(test)]
mod tests {
#[test]
fn topics_include_fledge_template() {
let mut topics: Vec<String> = vec!["rust".to_string(), "cli".to_string()];
if !topics.iter().any(|t| t == "fledge-template") {
topics.push("fledge-template".to_string());
}
assert!(topics.contains(&"fledge-template".to_string()));
let mut topics2: Vec<String> = vec!["fledge-template".to_string(), "rust".to_string()];
if !topics2.iter().any(|t| t == "fledge-template") {
topics2.push("fledge-template".to_string());
}
assert_eq!(
topics2.iter().filter(|t| *t == "fledge-template").count(),
1
);
}
#[test]
fn create_repo_request_body() {
let body = serde_json::json!({
"name": "my-template",
"description": "A cool template",
"private": false,
"auto_init": false,
});
assert_eq!(body["name"], "my-template");
assert_eq!(body["description"], "A cool template");
assert_eq!(body["private"], false);
assert_eq!(body["auto_init"], false);
}
#[test]
fn create_repo_org_request_url() {
let url = match Some("CorvidLabs") {
Some(o) => format!("https://api.github.com/orgs/{}/repos", o),
None => "https://api.github.com/user/repos".to_string(),
};
assert_eq!(url, "https://api.github.com/orgs/CorvidLabs/repos");
let personal_url = match None::<&str> {
Some(o) => format!("https://api.github.com/orgs/{}/repos", o),
None => "https://api.github.com/user/repos".to_string(),
};
assert_eq!(personal_url, "https://api.github.com/user/repos");
}
#[test]
#[ignore] fn publish_live() {
}
}