Merge pull request 'add wiki commands' (#105) from wiki into main

Reviewed-on: https://codeberg.org/Cyborus/forgejo-cli/pulls/105
This commit is contained in:
Cyborus 2024-08-07 23:45:51 +00:00
commit 96f72ee428
3 changed files with 229 additions and 59 deletions

View file

@ -13,6 +13,7 @@ mod prs;
mod release; mod release;
mod repo; mod repo;
mod user; mod user;
mod wiki;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
pub struct App { pub struct App {
@ -30,6 +31,7 @@ pub enum Command {
Repo(repo::RepoCommand), Repo(repo::RepoCommand),
Issue(issues::IssueCommand), Issue(issues::IssueCommand),
Pr(prs::PrCommand), Pr(prs::PrCommand),
Wiki(wiki::WikiCommand),
#[command(name = "whoami")] #[command(name = "whoami")]
WhoAmI { WhoAmI {
#[clap(long, short)] #[clap(long, short)]
@ -61,6 +63,7 @@ async fn main() -> eyre::Result<()> {
Command::Repo(subcommand) => subcommand.run(&mut keys, host_name).await?, Command::Repo(subcommand) => subcommand.run(&mut keys, host_name).await?,
Command::Issue(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::Pr(subcommand) => subcommand.run(&mut keys, host_name).await?,
Command::Wiki(subcommand) => subcommand.run(&mut keys, host_name).await?,
Command::WhoAmI { remote } => { Command::WhoAmI { remote } => {
let url = repo::RepoInfo::get_current(host_name, None, remote.as_deref()) let url = repo::RepoInfo::get_current(host_name, None, remote.as_deref())
.wrap_err("could not find host, try specifying with --host")? .wrap_err("could not find host, try specifying with --host")?

View file

@ -600,65 +600,7 @@ impl RepoCommand {
let path = path.unwrap_or_else(|| PathBuf::from(format!("./{repo_name}"))); let path = path.unwrap_or_else(|| PathBuf::from(format!("./{repo_name}")));
let SpecialRender { let local_repo = clone_repo(&repo_full_name, &clone_url, &path)?;
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());
if let Some(parent) = repo_data.parent.as_deref() { if let Some(parent) = repo_data.parent.as_deref() {
let parent_clone_url = parent let parent_clone_url = parent
@ -717,3 +659,70 @@ impl RepoCommand {
Ok(()) Ok(())
} }
} }
pub fn clone_repo(
repo_name: &str,
url: &url::Url,
path: &std::path::Path,
) -> eyre::Result<git2::Repository> {
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)
}

158
src/wiki.rs Normal file
View file

@ -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<String>,
#[clap(subcommand)]
command: WikiSubcommand,
}
#[derive(Subcommand, Clone, Debug)]
pub enum WikiSubcommand {
Contents {
repo: Option<RepoArg>,
},
View {
#[clap(long, short)]
repo: Option<RepoArg>,
page: String,
},
Clone {
repo: Option<RepoArg>,
#[clap(long, short)]
path: Option<PathBuf>,
},
Browse {
#[clap(long, short)]
repo: Option<RepoArg>,
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<PathBuf>) -> 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(())
}