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",
|
"clap",
|
||||||
"colored",
|
"colored",
|
||||||
"dialoguer",
|
"dialoguer",
|
||||||
|
"dirs",
|
||||||
"keyring",
|
"keyring",
|
||||||
"octocrab",
|
"octocrab",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -697,6 +699,27 @@ dependencies = [
|
|||||||
"subtle",
|
"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]]
|
[[package]]
|
||||||
name = "displaydoc"
|
name = "displaydoc"
|
||||||
version = "0.2.6"
|
version = "0.2.6"
|
||||||
@@ -1538,6 +1561,15 @@ version = "0.2.16"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libredox"
|
||||||
|
version = "0.1.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@@ -1757,6 +1789,12 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "option-ext"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ordered-stream"
|
name = "ordered-stream"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -2111,6 +2149,17 @@ dependencies = [
|
|||||||
"bitflags",
|
"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]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.13.0"
|
version = "1.13.0"
|
||||||
@@ -2505,6 +2554,15 @@ dependencies = [
|
|||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "serde_urlencoded"
|
name = "serde_urlencoded"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@@ -2856,6 +2914,21 @@ dependencies = [
|
|||||||
"tokio",
|
"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]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "1.1.1+spec-1.1.0"
|
version = "1.1.1+spec-1.1.0"
|
||||||
@@ -2886,6 +2959,12 @@ dependencies = [
|
|||||||
"winnow",
|
"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]]
|
[[package]]
|
||||||
name = "tower"
|
name = "tower"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
|
|||||||
@@ -13,3 +13,5 @@ octocrab = "0.54.0"
|
|||||||
dialoguer = "0.12"
|
dialoguer = "0.12"
|
||||||
keyring = "4.1.4"
|
keyring = "4.1.4"
|
||||||
colored = "3.1.1"
|
colored = "3.1.1"
|
||||||
|
toml = "1.1.2+spec-1.1.0"
|
||||||
|
dirs = "6.0.0"
|
||||||
|
|||||||
@@ -26,23 +26,39 @@
|
|||||||
...
|
...
|
||||||
}@inputs:
|
}@inputs:
|
||||||
let
|
let
|
||||||
system = "aarch64-darwin";
|
systems = [
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
"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
|
in
|
||||||
{
|
{
|
||||||
devShells.${system}.default = devenv.lib.mkShell {
|
devShells = eachSystem (
|
||||||
|
system: pkgs: {
|
||||||
|
default = devenv.lib.mkShell {
|
||||||
inherit inputs pkgs;
|
inherit inputs pkgs;
|
||||||
modules = [
|
modules = [
|
||||||
./devenv.nix
|
./devenv.nix
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
packages.${system}.default = naerskLib.buildPackage {
|
packages = eachSystem (
|
||||||
|
system: pkgs:
|
||||||
|
let
|
||||||
|
naerskLib = pkgs.callPackage naersk { };
|
||||||
|
in
|
||||||
|
{
|
||||||
|
default = naerskLib.buildPackage {
|
||||||
name = "clonee";
|
name = "clonee";
|
||||||
version = "1.0";
|
version = "1.0.0";
|
||||||
src = ./.;
|
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::{
|
use crate::{config::parse_config, providers::Provider, tui::user_select_repo};
|
||||||
providers::{Provider, github::Github},
|
|
||||||
tui::user_select_repo,
|
|
||||||
};
|
|
||||||
use colored::*;
|
use colored::*;
|
||||||
use dialoguer::{Select, theme::ColorfulTheme};
|
use dialoguer::{Select, theme::ColorfulTheme};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::{io, process};
|
use std::{io, process};
|
||||||
|
|
||||||
|
mod config;
|
||||||
mod providers;
|
mod providers;
|
||||||
mod tui;
|
mod tui;
|
||||||
|
|
||||||
|
const CONFIG_PATH: &str = "~/.config/clonee";
|
||||||
|
|
||||||
enum DefaultAnswer {
|
enum DefaultAnswer {
|
||||||
Yes,
|
Yes,
|
||||||
No,
|
No,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ask_yes_no(message: &str, default: Option<DefaultAnswer>) -> bool {
|
fn ask_option<T>(message: &str, options: Vec<T>) -> T
|
||||||
let options = match default {
|
where
|
||||||
Some(DefaultAnswer::Yes) => vec!["Yes", "No"],
|
T: std::fmt::Display + Clone,
|
||||||
Some(DefaultAnswer::No) => vec!["No", "Yes"],
|
{
|
||||||
None => vec!["Yes", "No"],
|
|
||||||
};
|
|
||||||
|
|
||||||
let selection = Select::with_theme(&ColorfulTheme::default())
|
let selection = Select::with_theme(&ColorfulTheme::default())
|
||||||
.with_prompt(message.to_string())
|
.with_prompt(message.to_string())
|
||||||
.items(&options)
|
.items(&options)
|
||||||
@@ -28,7 +26,18 @@ fn ask_yes_no(message: &str, default: Option<DefaultAnswer>) -> bool {
|
|||||||
.interact()
|
.interact()
|
||||||
.unwrap();
|
.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,
|
"Yes" => true,
|
||||||
"No" => false,
|
"No" => false,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
@@ -47,12 +56,24 @@ fn get_input(message: &str) -> String {
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
// Testing to see if github provider works.
|
let config_file = format!("{}/config.toml", CONFIG_PATH);
|
||||||
let gh = Github {
|
let config = match parse_config(config_file.clone()).await {
|
||||||
username: "pure-sagacity".to_string(),
|
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 {
|
let selection = match user_select_repo(repos).await {
|
||||||
Ok(r) => r,
|
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 => {
|
true => {
|
||||||
let folder: Option<String> =
|
let folder: Option<String> =
|
||||||
if ask_yes_no("Use a custom folder?", Some(DefaultAnswer::No)) {
|
if ask_yes_no("Use a custom folder?", Some(DefaultAnswer::No)) {
|
||||||
@@ -72,7 +93,7 @@ async fn main() {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
match gh.clone(selection, folder, false).await {
|
match provider.clone(selection, folder, false).await {
|
||||||
Ok(_) => println!("Repo cloned!"),
|
Ok(_) => println!("Repo cloned!"),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
println!("{}", "Error cloning repo.".red())
|
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,
|
pub username: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Github {
|
||||||
|
fn new(username: String) -> Self {
|
||||||
|
Self { username }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 1. This struct matches GitHub's exact API shape.
|
// 1. This struct matches GitHub's exact API shape.
|
||||||
// It remains completely hidden inside your GitHub provider module.
|
// It remains completely hidden inside your GitHub provider module.
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod github;
|
pub mod github;
|
||||||
|
pub mod gitea;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
|||||||
Reference in New Issue
Block a user