generated from pure_sagacity/rust-starter
added config parsing, and the selection between multiple providers.
This commit is contained in:
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"cSpell.words": ["Gitea"]
|
||||
}
|
||||
Generated
+79
@@ -488,12 +488,14 @@ dependencies = [
|
||||
"clap",
|
||||
"colored",
|
||||
"dialoguer",
|
||||
"dirs",
|
||||
"keyring",
|
||||
"octocrab",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -697,6 +699,27 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.6"
|
||||
@@ -1538,6 +1561,15 @@ version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
@@ -1757,6 +1789,12 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-stream"
|
||||
version = "0.2.0"
|
||||
@@ -2111,6 +2149,17 @@ dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
"libredox",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.13.0"
|
||||
@@ -2505,6 +2554,15 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
@@ -2856,6 +2914,21 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde_core",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
@@ -2886,6 +2959,12 @@ dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.3"
|
||||
|
||||
@@ -13,3 +13,5 @@ octocrab = "0.54.0"
|
||||
dialoguer = "0.12"
|
||||
keyring = "4.1.4"
|
||||
colored = "3.1.1"
|
||||
toml = "1.1.2+spec-1.1.0"
|
||||
dirs = "6.0.0"
|
||||
|
||||
@@ -26,23 +26,39 @@
|
||||
...
|
||||
}@inputs:
|
||||
let
|
||||
system = "aarch64-darwin";
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
systems = [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
"x86_64-darwin"
|
||||
"aarch64-darwin"
|
||||
];
|
||||
|
||||
naerskLib = pkgs.callPackage naersk { };
|
||||
eachSystem = f: nixpkgs.lib.genAttrs systems (system: f system (nixpkgs.legacyPackages.${system}));
|
||||
in
|
||||
{
|
||||
devShells.${system}.default = devenv.lib.mkShell {
|
||||
inherit inputs pkgs;
|
||||
modules = [
|
||||
./devenv.nix
|
||||
];
|
||||
};
|
||||
devShells = eachSystem (
|
||||
system: pkgs: {
|
||||
default = devenv.lib.mkShell {
|
||||
inherit inputs pkgs;
|
||||
modules = [
|
||||
./devenv.nix
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
packages.${system}.default = naerskLib.buildPackage {
|
||||
name = "clonee";
|
||||
version = "1.0";
|
||||
src = ./.;
|
||||
};
|
||||
packages = eachSystem (
|
||||
system: pkgs:
|
||||
let
|
||||
naerskLib = pkgs.callPackage naersk { };
|
||||
in
|
||||
{
|
||||
default = naerskLib.buildPackage {
|
||||
name = "clonee";
|
||||
version = "1.0.0";
|
||||
src = ./.;
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::providers::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
pub provider: Vec<Provider>,
|
||||
}
|
||||
|
||||
// Added #[serde(tag = "type")] so Serde knows to look for a "type" field
|
||||
// instead of nesting the entire structure inside the variant name.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Provider {
|
||||
#[serde(rename = "github")]
|
||||
Github {
|
||||
friendly_name: String,
|
||||
username: String,
|
||||
token: Option<String>,
|
||||
},
|
||||
#[serde(rename = "gitea")]
|
||||
Gitea {
|
||||
friendly_name: String,
|
||||
base_url: String,
|
||||
username: String,
|
||||
token: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
pub enum ProviderClient {
|
||||
Github(crate::providers::github::Github),
|
||||
Gitea(crate::providers::gitea::Gitea),
|
||||
}
|
||||
|
||||
impl crate::providers::Provider for ProviderClient {
|
||||
async fn repositories(&self) -> crate::providers::Result<Vec<crate::providers::Repository>> {
|
||||
match self {
|
||||
ProviderClient::Github(g) => g.repositories().await,
|
||||
ProviderClient::Gitea(g) => g.repositories().await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn clone(
|
||||
&self,
|
||||
repo: crate::providers::Repository,
|
||||
folder: Option<String>,
|
||||
use_ssh: bool,
|
||||
) -> crate::providers::Result<()> {
|
||||
match self {
|
||||
ProviderClient::Github(g) => g.clone(repo, folder, use_ssh).await,
|
||||
ProviderClient::Gitea(g) => g.clone(repo, folder, use_ssh).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Provider {
|
||||
pub fn friendly_name(&self) -> &str {
|
||||
match self {
|
||||
Provider::Github { friendly_name, .. } => friendly_name,
|
||||
Provider::Gitea { friendly_name, .. } => friendly_name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_provider_trait(self) -> ProviderClient {
|
||||
match self {
|
||||
Provider::Github {
|
||||
username, token, ..
|
||||
} => {
|
||||
todo!("Return ProviderClient::Github(...)")
|
||||
}
|
||||
Provider::Gitea {
|
||||
base_url,
|
||||
username,
|
||||
token,
|
||||
..
|
||||
} => ProviderClient::Gitea(crate::providers::gitea::Gitea::new(
|
||||
base_url, username, token,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Provider {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.friendly_name())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn parse_config(file_path: String) -> Result<Config> {
|
||||
let mut path = PathBuf::from(file_path);
|
||||
|
||||
if path.starts_with("~") {
|
||||
if let Some(home_dir) = dirs::home_dir() {
|
||||
// Strip the "~" and join the rest of the path to the home directory
|
||||
path = home_dir.join(path.strip_prefix("~").unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
let config = fs::read_to_string(&path).await?;
|
||||
let config: Config = toml::from_str(&config)?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
+39
-18
@@ -1,26 +1,24 @@
|
||||
use crate::{
|
||||
providers::{Provider, github::Github},
|
||||
tui::user_select_repo,
|
||||
};
|
||||
use crate::{config::parse_config, providers::Provider, tui::user_select_repo};
|
||||
use colored::*;
|
||||
use dialoguer::{Select, theme::ColorfulTheme};
|
||||
use std::io::Write;
|
||||
use std::{io, process};
|
||||
|
||||
mod config;
|
||||
mod providers;
|
||||
mod tui;
|
||||
|
||||
const CONFIG_PATH: &str = "~/.config/clonee";
|
||||
|
||||
enum DefaultAnswer {
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
fn ask_yes_no(message: &str, default: Option<DefaultAnswer>) -> bool {
|
||||
let options = match default {
|
||||
Some(DefaultAnswer::Yes) => vec!["Yes", "No"],
|
||||
Some(DefaultAnswer::No) => vec!["No", "Yes"],
|
||||
None => vec!["Yes", "No"],
|
||||
};
|
||||
|
||||
fn ask_option<T>(message: &str, options: Vec<T>) -> T
|
||||
where
|
||||
T: std::fmt::Display + Clone,
|
||||
{
|
||||
let selection = Select::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt(message.to_string())
|
||||
.items(&options)
|
||||
@@ -28,7 +26,18 @@ fn ask_yes_no(message: &str, default: Option<DefaultAnswer>) -> bool {
|
||||
.interact()
|
||||
.unwrap();
|
||||
|
||||
match options[selection] {
|
||||
options[selection].clone()
|
||||
}
|
||||
|
||||
fn ask_yes_no(message: &str, default: Option<DefaultAnswer>) -> bool {
|
||||
let options: Vec<&str> = match default {
|
||||
Some(DefaultAnswer::Yes) => vec!["Yes", "No"],
|
||||
Some(DefaultAnswer::No) => vec!["No", "Yes"],
|
||||
None => vec!["Yes", "No"],
|
||||
};
|
||||
|
||||
// &str implements both Display and Copy!
|
||||
match ask_option(message, options) {
|
||||
"Yes" => true,
|
||||
"No" => false,
|
||||
_ => unreachable!(),
|
||||
@@ -47,12 +56,24 @@ fn get_input(message: &str) -> String {
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Testing to see if github provider works.
|
||||
let gh = Github {
|
||||
username: "pure-sagacity".to_string(),
|
||||
let config_file = format!("{}/config.toml", CONFIG_PATH);
|
||||
let config = match parse_config(config_file.clone()).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
let top = "Failed to parse config.".red();
|
||||
let bottom = "Maybe there's an error in the config?".dimmed();
|
||||
|
||||
println!("{}\n{}", top, bottom);
|
||||
eprintln!("Error at {}: {}", config_file, e);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let repos = gh.repositories().await.expect("Failed to get repos.");
|
||||
let selected_provider = ask_option("Choose a provider:", config.provider);
|
||||
|
||||
let provider: crate::config::ProviderClient = selected_provider.into_provider_trait();
|
||||
|
||||
let repos = provider.repositories().await.expect("Failed to get repos.");
|
||||
|
||||
let selection = match user_select_repo(repos).await {
|
||||
Ok(r) => r,
|
||||
@@ -62,7 +83,7 @@ async fn main() {
|
||||
}
|
||||
};
|
||||
|
||||
match ask_yes_no("Clone the repo?", None) {
|
||||
match ask_yes_no("Clone the repo?", Some(DefaultAnswer::Yes)) {
|
||||
true => {
|
||||
let folder: Option<String> =
|
||||
if ask_yes_no("Use a custom folder?", Some(DefaultAnswer::No)) {
|
||||
@@ -72,7 +93,7 @@ async fn main() {
|
||||
None
|
||||
};
|
||||
|
||||
match gh.clone(selection, folder, false).await {
|
||||
match provider.clone(selection, folder, false).await {
|
||||
Ok(_) => println!("Repo cloned!"),
|
||||
Err(_) => {
|
||||
println!("{}", "Error cloning repo.".red())
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
use super::{Provider, Repository, Result};
|
||||
use serde::Deserialize;
|
||||
|
||||
pub struct Gitea {
|
||||
pub base_url: String,
|
||||
pub username: String,
|
||||
pub token: Option<String>,
|
||||
}
|
||||
|
||||
impl Gitea {
|
||||
pub fn new(base_url: String, username: String, token: Option<String>) -> Self {
|
||||
Self {
|
||||
base_url,
|
||||
username,
|
||||
token,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Matches Gitea's API response schema exactly
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct GiteaRepoDto {
|
||||
id: u64,
|
||||
name: String,
|
||||
owner: GiteaOwnerDto,
|
||||
ssh_url: String,
|
||||
clone_url: String,
|
||||
stars_count: u64,
|
||||
description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct GiteaOwnerDto {
|
||||
login: String,
|
||||
}
|
||||
|
||||
// 2. Map the Gitea DTO to your generic Repository struct
|
||||
impl From<GiteaRepoDto> for Repository {
|
||||
fn from(gt: GiteaRepoDto) -> Self {
|
||||
Repository {
|
||||
id: gt.id,
|
||||
name: gt.name,
|
||||
owner: gt.owner.login,
|
||||
ssh_url: Some(gt.ssh_url),
|
||||
https_url: gt.clone_url,
|
||||
stars: gt.stars_count,
|
||||
description: gt.description,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Provider for Gitea {
|
||||
async fn repositories(&self) -> Result<Vec<Repository>> {
|
||||
let client = reqwest::Client::new();
|
||||
let base_endpoint = self.base_url.trim_end_matches('/');
|
||||
|
||||
// Route to the authenticated user's endpoint if a token exists,
|
||||
// otherwise route to the target user's public repositories endpoint.
|
||||
let url = match self.token.is_some() {
|
||||
true => format!("{}/api/v1/user/repos", base_endpoint),
|
||||
false => format!("{}/api/v1/users/{}/repos", base_endpoint, self.username),
|
||||
};
|
||||
|
||||
let mut request = client.get(&url).header("Accept", "application/json");
|
||||
|
||||
if let Some(ref token) = self.token {
|
||||
request = request.bearer_auth(token);
|
||||
}
|
||||
|
||||
let response = request.send().await?;
|
||||
let status = response.status();
|
||||
|
||||
if !status.is_success() {
|
||||
let error_text = response.text().await?;
|
||||
let err = format!("Gitea API Error ({}): {}", status, error_text);
|
||||
return Err(err.into());
|
||||
}
|
||||
|
||||
let gitea_repos: Vec<GiteaRepoDto> = response.json().await?;
|
||||
|
||||
// Convert the Gitea DTOs seamlessly into your generic repository structs
|
||||
let repos = gitea_repos.into_iter().map(Repository::from).collect();
|
||||
|
||||
Ok(repos)
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,12 @@ pub struct Github {
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
impl Github {
|
||||
fn new(username: String) -> Self {
|
||||
Self { username }
|
||||
}
|
||||
}
|
||||
|
||||
// 1. This struct matches GitHub's exact API shape.
|
||||
// It remains completely hidden inside your GitHub provider module.
|
||||
#[derive(Deserialize, Debug)]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod github;
|
||||
pub mod gitea;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::process::Command;
|
||||
|
||||
Reference in New Issue
Block a user