From eadadf8dd81522b3bdb6ab2c5b7cd3f71cce45cf Mon Sep 17 00:00:00 2001 From: Cyborus Date: Sun, 4 Aug 2024 11:28:41 -0400 Subject: [PATCH 1/6] chore: wiki file --- src/main.rs | 3 +++ src/wiki.rs | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 src/wiki.rs 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/wiki.rs b/src/wiki.rs new file mode 100644 index 0000000..91792ad --- /dev/null +++ b/src/wiki.rs @@ -0,0 +1,41 @@ +use clap::{Args, Subcommand}; + +use crate::repo::{RepoArg, RepoInfo}; + +#[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 {} + +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(|| self.no_repo_error())?; + + match self.command {} + } + + fn repo(&self) -> Option<&RepoArg> { + use WikiSubcommand::*; + match &self.command { + _ => todo!(), + } + } + + fn no_repo_error(&self) -> eyre::Error { + use WikiSubcommand::*; + match &self.command { + _ => todo!(), + } + } +} From 296ac0c8531ae98891cb532cba2d4cb8fd66698d Mon Sep 17 00:00:00 2001 From: Cyborus Date: Mon, 5 Aug 2024 13:40:49 -0400 Subject: [PATCH 2/6] feat: `wiki contents` --- src/wiki.rs | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/src/wiki.rs b/src/wiki.rs index 91792ad..9812346 100644 --- a/src/wiki.rs +++ b/src/wiki.rs @@ -1,6 +1,11 @@ use clap::{Args, Subcommand}; +use eyre::OptionExt; +use forgejo_api::Forgejo; -use crate::repo::{RepoArg, RepoInfo}; +use crate::{ + repo::{RepoArg, RepoInfo, RepoName}, + SpecialRender, +}; #[derive(Args, Clone, Debug)] pub struct WikiCommand { @@ -12,7 +17,9 @@ pub struct WikiCommand { } #[derive(Subcommand, Clone, Debug)] -pub enum WikiSubcommand {} +pub enum WikiSubcommand { + Contents { repo: Option }, +} impl WikiCommand { pub async fn run(self, keys: &mut crate::KeyInfo, host_name: Option<&str>) -> eyre::Result<()> { @@ -22,20 +29,44 @@ impl WikiCommand { let api = keys.get_api(repo.host_url()).await?; let repo = repo.name().ok_or_else(|| self.no_repo_error())?; - match self.command {} + match self.command { + Contents { repo: _ } => wiki_contents(&repo, &api).await?, + } + Ok(()) } fn repo(&self) -> Option<&RepoArg> { use WikiSubcommand::*; match &self.command { - _ => todo!(), + Contents { repo } => repo.as_ref(), } } fn no_repo_error(&self) -> eyre::Error { use WikiSubcommand::*; match &self.command { - _ => todo!(), + Contents { repo: _ } => eyre::eyre!("couldn't guess repo"), } } } + +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(()) +} From a756d1da5745cd72120d2199d19703961203a2f1 Mon Sep 17 00:00:00 2001 From: Cyborus Date: Mon, 5 Aug 2024 15:16:37 -0400 Subject: [PATCH 3/6] feat: `wiki view` --- src/wiki.rs | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/src/wiki.rs b/src/wiki.rs index 9812346..f5724df 100644 --- a/src/wiki.rs +++ b/src/wiki.rs @@ -1,5 +1,6 @@ +use base64ct::Encoding; use clap::{Args, Subcommand}; -use eyre::OptionExt; +use eyre::{Context, OptionExt}; use forgejo_api::Forgejo; use crate::{ @@ -18,7 +19,14 @@ pub struct WikiCommand { #[derive(Subcommand, Clone, Debug)] pub enum WikiSubcommand { - Contents { repo: Option }, + Contents { + repo: Option, + }, + View { + #[clap(long, short)] + repo: Option, + page: String, + }, } impl WikiCommand { @@ -31,6 +39,7 @@ impl WikiCommand { match self.command { Contents { repo: _ } => wiki_contents(&repo, &api).await?, + View { repo: _, page } => view_wiki_page(&repo, &api, &*page).await?, } Ok(()) } @@ -38,14 +47,14 @@ impl WikiCommand { fn repo(&self) -> Option<&RepoArg> { use WikiSubcommand::*; match &self.command { - Contents { repo } => repo.as_ref(), + Contents { repo } | View { repo, .. } => repo.as_ref(), } } fn no_repo_error(&self) -> eyre::Error { use WikiSubcommand::*; match &self.command { - Contents { repo: _ } => eyre::eyre!("couldn't guess repo"), + Contents { repo: _ } | View { .. } => eyre::eyre!("couldn't guess repo"), } } } @@ -70,3 +79,28 @@ async fn wiki_contents(repo: &RepoName, api: &Forgejo) -> eyre::Result<()> { 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(()) +} From de144f29e663d5d1aaebbf608cf794dd2c3323c9 Mon Sep 17 00:00:00 2001 From: Cyborus Date: Wed, 7 Aug 2024 18:19:57 -0400 Subject: [PATCH 4/6] feat: `wiki browse` --- src/wiki.rs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/wiki.rs b/src/wiki.rs index f5724df..4aff76c 100644 --- a/src/wiki.rs +++ b/src/wiki.rs @@ -27,6 +27,11 @@ pub enum WikiSubcommand { repo: Option, page: String, }, + Browse { + #[clap(long, short)] + repo: Option, + page: String, + }, } impl WikiCommand { @@ -40,6 +45,7 @@ impl WikiCommand { match self.command { Contents { repo: _ } => wiki_contents(&repo, &api).await?, View { repo: _, page } => view_wiki_page(&repo, &api, &*page).await?, + Browse { repo: _, page } => browse_wiki_page(&repo, &api, &*page).await?, } Ok(()) } @@ -47,14 +53,16 @@ impl WikiCommand { fn repo(&self) -> Option<&RepoArg> { use WikiSubcommand::*; match &self.command { - Contents { repo } | View { repo, .. } => repo.as_ref(), + Contents { repo } | View { repo, .. } | Browse { repo, .. } => repo.as_ref(), } } fn no_repo_error(&self) -> eyre::Error { use WikiSubcommand::*; match &self.command { - Contents { repo: _ } | View { .. } => eyre::eyre!("couldn't guess repo"), + Contents { repo: _ } | View { .. } | Browse { .. } => { + eyre::eyre!("couldn't guess repo") + } } } } @@ -104,3 +112,15 @@ async fn view_wiki_page(repo: &RepoName, api: &Forgejo, page: &str) -> eyre::Res 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(()) +} From 13b7bf53053fb7fb518108e1886ba1f33d7b28b7 Mon Sep 17 00:00:00 2001 From: Cyborus Date: Wed, 7 Aug 2024 19:03:50 -0400 Subject: [PATCH 5/6] feat: `wiki clone` --- src/repo.rs | 127 ++++++++++++++++++++++++++++------------------------ src/wiki.rs | 43 +++++++++++++++++- 2 files changed, 109 insertions(+), 61 deletions(-) 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 index 4aff76c..3f31023 100644 --- a/src/wiki.rs +++ b/src/wiki.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use base64ct::Encoding; use clap::{Args, Subcommand}; use eyre::{Context, OptionExt}; @@ -27,6 +29,11 @@ pub enum WikiSubcommand { repo: Option, page: String, }, + Clone { + repo: Option, + #[clap(long, short)] + path: Option, + }, Browse { #[clap(long, short)] repo: Option, @@ -45,6 +52,7 @@ impl WikiCommand { 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(()) @@ -53,14 +61,16 @@ impl WikiCommand { fn repo(&self) -> Option<&RepoArg> { use WikiSubcommand::*; match &self.command { - Contents { repo } | View { repo, .. } | Browse { repo, .. } => repo.as_ref(), + Contents { repo } | View { repo, .. } | Clone { repo, .. } | Browse { repo, .. } => { + repo.as_ref() + } } } fn no_repo_error(&self) -> eyre::Error { use WikiSubcommand::*; match &self.command { - Contents { repo: _ } | View { .. } | Browse { .. } => { + Contents { repo: _ } | View { .. } | Clone { .. } | Browse { .. } => { eyre::eyre!("couldn't guess repo") } } @@ -124,3 +134,32 @@ async fn browse_wiki_page(repo: &RepoName, api: &Forgejo, page: &str) -> eyre::R 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(()) +} From 09d995579f60485674fd928575d903139d77a4c4 Mon Sep 17 00:00:00 2001 From: Cyborus Date: Wed, 7 Aug 2024 19:41:15 -0400 Subject: [PATCH 6/6] refactor: remove `WikiCommand::no_repo_error` --- src/wiki.rs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/wiki.rs b/src/wiki.rs index 3f31023..63d344f 100644 --- a/src/wiki.rs +++ b/src/wiki.rs @@ -47,7 +47,9 @@ impl WikiCommand { 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(|| self.no_repo_error())?; + let repo = repo + .name() + .ok_or_else(|| eyre::eyre!("couldn't guess repo"))?; match self.command { Contents { repo: _ } => wiki_contents(&repo, &api).await?, @@ -66,15 +68,6 @@ impl WikiCommand { } } } - - fn no_repo_error(&self) -> eyre::Error { - use WikiSubcommand::*; - match &self.command { - Contents { repo: _ } | View { .. } | Clone { .. } | Browse { .. } => { - eyre::eyre!("couldn't guess repo") - } - } - } } async fn wiki_contents(repo: &RepoName, api: &Forgejo) -> eyre::Result<()> {