diff --git a/src/main.rs b/src/main.rs index 59a70da..c26081f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ mod prs; mod release; mod repo; mod user; +mod wiki; #[derive(Parser, Debug)] pub struct App { @@ -30,6 +31,7 @@ pub enum Command { Repo(repo::RepoCommand), Issue(issues::IssueCommand), Pr(prs::PrCommand), + Wiki(wiki::WikiCommand), #[command(name = "whoami")] WhoAmI { #[clap(long, short)] @@ -61,6 +63,7 @@ async fn main() -> eyre::Result<()> { Command::Repo(subcommand) => subcommand.run(&mut keys, host_name).await?, Command::Issue(subcommand) => subcommand.run(&mut keys, host_name).await?, Command::Pr(subcommand) => subcommand.run(&mut keys, host_name).await?, + Command::Wiki(subcommand) => subcommand.run(&mut keys, host_name).await?, Command::WhoAmI { remote } => { let url = repo::RepoInfo::get_current(host_name, None, remote.as_deref()) .wrap_err("could not find host, try specifying with --host")? diff --git a/src/repo.rs b/src/repo.rs index ee67b9b..593caba 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -600,65 +600,7 @@ impl RepoCommand { let path = path.unwrap_or_else(|| PathBuf::from(format!("./{repo_name}"))); - let SpecialRender { - fancy, - hide_cursor, - show_cursor, - clear_line, - .. - } = *crate::special_render(); - - let auth = auth_git2::GitAuthenticator::new(); - let git_config = git2::Config::open_default()?; - - let mut options = git2::FetchOptions::new(); - let mut callbacks = git2::RemoteCallbacks::new(); - callbacks.credentials(auth.credentials(&git_config)); - - if fancy { - print!("{hide_cursor}"); - print!(" Preparing..."); - let _ = std::io::stdout().flush(); - - callbacks.transfer_progress(|progress| { - print!("{clear_line}\r"); - if progress.received_objects() == progress.total_objects() { - if progress.indexed_deltas() == progress.total_deltas() { - print!("Finishing up..."); - } else { - let percent = 100.0 * (progress.indexed_deltas() as f64) - / (progress.total_deltas() as f64); - print!(" Resolving... {percent:.01}%"); - } - } else { - let bytes = progress.received_bytes(); - let percent = 100.0 * (progress.received_objects() as f64) - / (progress.total_objects() as f64); - print!(" Downloading... {percent:.01}%"); - match bytes { - 0..=1023 => print!(" ({}b)", bytes), - 1024..=1048575 => print!(" ({:.01}kb)", (bytes as f64) / 1024.0), - 1048576..=1073741823 => { - print!(" ({:.01}mb)", (bytes as f64) / 1048576.0) - } - 1073741824.. => { - print!(" ({:.01}gb)", (bytes as f64) / 1073741824.0) - } - } - } - let _ = std::io::stdout().flush(); - true - }); - options.remote_callbacks(callbacks); - } - - let local_repo = git2::build::RepoBuilder::new() - .fetch_options(options) - .clone(clone_url.as_str(), &path)?; - if fancy { - print!("{clear_line}{show_cursor}\r"); - } - println!("Cloned {} into {}", repo_full_name, path.display()); + let local_repo = clone_repo(&repo_full_name, &clone_url, &path)?; if let Some(parent) = repo_data.parent.as_deref() { let parent_clone_url = parent @@ -717,3 +659,70 @@ impl RepoCommand { Ok(()) } } + +pub fn clone_repo( + repo_name: &str, + url: &url::Url, + path: &std::path::Path, +) -> eyre::Result { + let SpecialRender { + fancy, + hide_cursor, + show_cursor, + clear_line, + .. + } = *crate::special_render(); + + let auth = auth_git2::GitAuthenticator::new(); + let git_config = git2::Config::open_default()?; + + let mut options = git2::FetchOptions::new(); + let mut callbacks = git2::RemoteCallbacks::new(); + callbacks.credentials(auth.credentials(&git_config)); + + if fancy { + print!("{hide_cursor}"); + print!(" Preparing..."); + let _ = std::io::stdout().flush(); + + callbacks.transfer_progress(|progress| { + print!("{clear_line}\r"); + if progress.received_objects() == progress.total_objects() { + if progress.indexed_deltas() == progress.total_deltas() { + print!("Finishing up..."); + } else { + let percent = 100.0 * (progress.indexed_deltas() as f64) + / (progress.total_deltas() as f64); + print!(" Resolving... {percent:.01}%"); + } + } else { + let bytes = progress.received_bytes(); + let percent = 100.0 * (progress.received_objects() as f64) + / (progress.total_objects() as f64); + print!(" Downloading... {percent:.01}%"); + match bytes { + 0..=1023 => print!(" ({}b)", bytes), + 1024..=1048575 => print!(" ({:.01}kb)", (bytes as f64) / 1024.0), + 1048576..=1073741823 => { + print!(" ({:.01}mb)", (bytes as f64) / 1048576.0) + } + 1073741824.. => { + print!(" ({:.01}gb)", (bytes as f64) / 1073741824.0) + } + } + } + let _ = std::io::stdout().flush(); + true + }); + options.remote_callbacks(callbacks); + } + + let local_repo = git2::build::RepoBuilder::new() + .fetch_options(options) + .clone(url.as_str(), &path)?; + if fancy { + print!("{clear_line}{show_cursor}\r"); + } + println!("Cloned {} into {}", repo_name, path.display()); + Ok(local_repo) +} diff --git a/src/wiki.rs b/src/wiki.rs new file mode 100644 index 0000000..63d344f --- /dev/null +++ b/src/wiki.rs @@ -0,0 +1,158 @@ +use std::path::PathBuf; + +use base64ct::Encoding; +use clap::{Args, Subcommand}; +use eyre::{Context, OptionExt}; +use forgejo_api::Forgejo; + +use crate::{ + repo::{RepoArg, RepoInfo, RepoName}, + SpecialRender, +}; + +#[derive(Args, Clone, Debug)] +pub struct WikiCommand { + /// The local git remote that points to the repo to operate on. + #[clap(long, short = 'R')] + remote: Option, + #[clap(subcommand)] + command: WikiSubcommand, +} + +#[derive(Subcommand, Clone, Debug)] +pub enum WikiSubcommand { + Contents { + repo: Option, + }, + View { + #[clap(long, short)] + repo: Option, + page: String, + }, + Clone { + repo: Option, + #[clap(long, short)] + path: Option, + }, + Browse { + #[clap(long, short)] + repo: Option, + page: String, + }, +} + +impl WikiCommand { + pub async fn run(self, keys: &mut crate::KeyInfo, host_name: Option<&str>) -> eyre::Result<()> { + use WikiSubcommand::*; + + let repo = RepoInfo::get_current(host_name, self.repo(), self.remote.as_deref())?; + let api = keys.get_api(repo.host_url()).await?; + let repo = repo + .name() + .ok_or_else(|| eyre::eyre!("couldn't guess repo"))?; + + match self.command { + Contents { repo: _ } => wiki_contents(&repo, &api).await?, + View { repo: _, page } => view_wiki_page(&repo, &api, &*page).await?, + Clone { repo: _, path } => clone_wiki(&repo, &api, path).await?, + Browse { repo: _, page } => browse_wiki_page(&repo, &api, &*page).await?, + } + Ok(()) + } + + fn repo(&self) -> Option<&RepoArg> { + use WikiSubcommand::*; + match &self.command { + Contents { repo } | View { repo, .. } | Clone { repo, .. } | Browse { repo, .. } => { + repo.as_ref() + } + } + } +} + +async fn wiki_contents(repo: &RepoName, api: &Forgejo) -> eyre::Result<()> { + let SpecialRender { bullet, .. } = *crate::special_render(); + + let query = forgejo_api::structs::RepoGetWikiPagesQuery { + page: None, + limit: None, + }; + let pages = api + .repo_get_wiki_pages(repo.owner(), repo.name(), query) + .await?; + for page in pages { + let title = page + .title + .as_deref() + .ok_or_eyre("page does not have title")?; + println!("{bullet} {title}"); + } + + Ok(()) +} + +async fn view_wiki_page(repo: &RepoName, api: &Forgejo, page: &str) -> eyre::Result<()> { + let SpecialRender { bold, reset, .. } = *crate::special_render(); + + let page = api + .repo_get_wiki_page(repo.owner(), repo.name(), page) + .await?; + + let title = page + .title + .as_deref() + .ok_or_eyre("page does not have title")?; + println!("{bold}{title}{reset}"); + println!(); + + let contents_b64 = page + .content_base64 + .as_deref() + .ok_or_eyre("page does not have content")?; + let contents = String::from_utf8(base64ct::Base64::decode_vec(contents_b64)?) + .wrap_err("page content is not utf-8")?; + + println!("{}", crate::markdown(&contents)); + Ok(()) +} + +async fn browse_wiki_page(repo: &RepoName, api: &Forgejo, page: &str) -> eyre::Result<()> { + let page = api + .repo_get_wiki_page(repo.owner(), repo.name(), page) + .await?; + let html_url = page + .html_url + .as_ref() + .ok_or_eyre("page does not have html url")?; + open::that(html_url.as_str())?; + Ok(()) +} + +async fn clone_wiki(repo: &RepoName, api: &Forgejo, path: Option) -> eyre::Result<()> { + let repo_data = api.repo_get(repo.owner(), repo.name()).await?; + let clone_url = repo_data + .clone_url + .as_ref() + .ok_or_eyre("repo does not have clone url")?; + let git_stripped = clone_url + .as_str() + .strip_suffix(".git") + .unwrap_or(clone_url.as_str()); + let clone_url = url::Url::parse(&format!("{}.wiki.git", git_stripped))?; + + let repo_name = repo_data + .name + .as_deref() + .ok_or_eyre("repo does not have name")?; + let repo_full_name = repo_data + .full_name + .as_deref() + .ok_or_eyre("repo does not have full name")?; + let name = format!("{}'s wiki", repo_full_name); + + let path = path.unwrap_or_else(|| PathBuf::from(format!("./{repo_name}-wiki"))); + + crate::repo::clone_repo(&name, &clone_url, &path)?; + + Ok(()) +}