Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 84 additions & 26 deletions src/github.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::collections::BTreeSet;
use std::collections::{BTreeSet, HashMap};
use std::process::Command;

use anyhow::Result;
Expand Down Expand Up @@ -82,7 +82,7 @@ struct Release {
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Report {
pub struct Report<M = String> {
title: String,

#[serde(skip_serializing_if = "Option::is_none")]
Expand All @@ -93,10 +93,13 @@ pub struct Report {

#[serde(skip_serializing_if = "Option::is_none")]
assignees: Option<Vec<String>>,

#[serde(skip_serializing_if = "Option::is_none")]
milestone: Option<M>,
}

#[derive(Debug, Clone, clap::Args)]
pub struct GitHub {
pub struct GitHubArgs {
/// GitHub token for API access
#[arg(long, env = "GITHUB_TOKEN")]
pub token: Option<String>,
Expand All @@ -105,7 +108,7 @@ pub struct GitHub {
#[arg(short = 'o', long)]
pub owner: String,

/// GitHub repository name
/// GitHub repository name
#[arg(short = 'r', long)]
pub repo: String,

Expand All @@ -118,11 +121,9 @@ pub struct GitHub {
pub filter: Vec<String>,
}

impl GitHub {
const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));

impl GitHubArgs {
/// Authenticate with GitHub by guiding user to create a Personal Access Token
pub fn login(&mut self) -> Result<()> {
pub async fn login(mut self) -> Result<GitHub> {
// Try to get token from GitHub CLI
if self.token.is_none() {
self.token = Command::new("gh")
Expand All @@ -142,9 +143,9 @@ impl GitHub {
});
}

// If we already have a token, nothing to do
// If we already have a token, create the client.
if self.token.is_some() {
return Ok(());
return GitHub::new(self).await;
}

println!("No GitHub token found. Please authenticate with GitHub.");
Expand All @@ -168,53 +169,110 @@ impl GitHub {

anyhow::bail!("GitHub authentication required. Please follow the instructions above.");
}
}

#[derive(Debug, Serialize, Deserialize)]
struct Milestone {
title: String,
number: u64,
}

fn client(&self) -> Client {
#[derive(Debug)]
pub struct GitHub {
args: GitHubArgs,
client: Client,
milestones: HashMap<String, u64>,
}

impl GitHub {
const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
const PER_PAGE: u32 = 100;

async fn new(args: GitHubArgs) -> Result<Self> {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert("Accept", "application/vnd.github+json".parse().unwrap());
headers.insert("User-Agent", Self::USER_AGENT.parse().unwrap());
headers.insert("Accept", "application/vnd.github+json".parse()?);
headers.insert("User-Agent", Self::USER_AGENT.parse()?);

if let Some(token) = &self.token {
if let Some(token) = &args.token {
let auth_value = format!("Bearer {token}");
headers.insert("Authorization", auth_value.parse().unwrap());
headers.insert("Authorization", auth_value.parse()?);
}

Client::builder()
.default_headers(headers)
.build()
.expect("Failed to create HTTP client")
let client = Client::builder().default_headers(headers).build()?;
let mut milestones = HashMap::new();

// Load all milestones...
for n in 1.. {
let url = format!(
"https://api.github.com/repos/{}/{}/milestones?state=all&per_page={}&page={}",
args.owner,
args.repo,
Self::PER_PAGE,
n
);

let response = client.get(&url).send().await?;
let page: Vec<Milestone> = response.json().await?;
if page.is_empty() {
break;
}

for milestone in page {
milestones.insert(milestone.title, milestone.number);
}
}

Ok(Self {
args,
client,
milestones,
})
}

pub async fn assets(&self) -> Result<BTreeSet<Asset>> {
let client = self.client();
let url = format!(
"https://api.github.com/repos/{}/{}/releases/tags/{}",
self.owner, self.repo, self.tag
self.args.owner, self.args.repo, self.args.tag
);

let response = client.get(&url).send().await?;
let response = self.client.get(&url).send().await?;
let release: Release = response.json().await?;

let assets = release
.assets
.into_iter()
.filter_map(Asset::known)
.filter(|asset| {
self.filter.is_empty() || self.filter.iter().any(|f| asset.name.contains(f))
self.args.filter.is_empty()
|| self.args.filter.iter().any(|f| asset.name.contains(f))
})
.collect::<BTreeSet<_>>();

Ok(assets)
}

fn milestone(&self, title: &str) -> Result<u64> {
self.milestones
.get(title)
.copied()
.ok_or_else(|| anyhow::anyhow!("Milestone '{}' not found", title))
}

pub async fn report(&self, report: Report) -> Result<()> {
let client = self.client();
let report = Report {
title: report.title,
body: report.body,
labels: report.labels,
assignees: report.assignees,
milestone: report.milestone.map(|t| self.milestone(&t)).transpose()?,
};

let url = format!(
"https://api.github.com/repos/{}/{}/issues",
self.owner, self.repo
self.args.owner, self.args.repo
);

client.post(&url).json(&report).send().await?;
self.client.post(&url).json(&report).send().await?;
Ok(())
}
}
13 changes: 5 additions & 8 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ mod tui;
use std::sync::Arc;

use crate::avahi::AvahiService;
use crate::github::GitHub;
use crate::github::GitHubArgs;
use crate::http::Server;
use crate::tui::{Status, Throbbing};

Expand All @@ -35,7 +35,7 @@ use tokio::sync::Mutex;
#[command(about = std::env!("CARGO_PKG_DESCRIPTION"))]
struct Args {
#[command(flatten)]
github: GitHub,
github: GitHubArgs,

/// Address and port to bind to
#[arg(short = 'b', long, default_value = "0.0.0.0:8080")]
Expand All @@ -47,22 +47,20 @@ struct Args {
}

#[tokio::main]
#[allow(clippy::too_many_lines)]
async fn main() -> Result<()> {
// Parse arguments
let mut args = Args::parse();
let args = Args::parse();

// Ensure we're authenticated with GitHub
args.github.login()?;
let github = Arc::new(args.github.login().await?);

// Bind to our server port
let listener = TcpListener::bind(&args.bind).await?;
let addr = listener.local_addr()?;
let path = Arc::new(args.path);

// Load the github assets
let assets = args
.github
let assets = github
.assets()
.throbbing("Loading GitHub assets...")
.await?;
Expand All @@ -72,7 +70,6 @@ async fn main() -> Result<()> {
status.lock().await.render()?;

// Create the HTTP server
let github = Arc::new(args.github);
let server = Server::new(listener, status.clone(), github, path.clone())?;

// Create TXT records
Expand Down
Loading