feat: Initial import
This commit is contained in:
commit
5cd0635832
5 changed files with 2112 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
1858
Cargo.lock
generated
Normal file
1858
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
11
Cargo.toml
Normal file
11
Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
[package]
|
||||||
|
name = "forgejo_automate"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4.5.39", features = ["cargo", "derive"] }
|
||||||
|
forgejo-api = "0.7.0"
|
||||||
|
rust-ini = "0.21.1"
|
||||||
|
tokio = { version = "1.45.1", features = ["macros", "rt-multi-thread"] }
|
||||||
|
url = "2.5.4"
|
118
src/config.rs
Normal file
118
src/config.rs
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
use ini::Ini;
|
||||||
|
// use rocket::tokio::sync::broadcast;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ConfigError {
|
||||||
|
IoError(std::io::Error),
|
||||||
|
IniError(ini::Error),
|
||||||
|
ParseError(String),
|
||||||
|
InvalidMacAddress(String),
|
||||||
|
InvalidIpAddress(String),
|
||||||
|
MissingRequiredField { host: String, field: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ConfigError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ConfigError::IoError(err) => write!(f, "IO error: {}", err),
|
||||||
|
ConfigError::IniError(err) => write!(f, "INI error: {}", err),
|
||||||
|
ConfigError::ParseError(msg) => write!(f, "Parse error: {}", msg),
|
||||||
|
ConfigError::InvalidMacAddress(mac) => write!(f, "Invalid MAC address: {}", mac),
|
||||||
|
ConfigError::InvalidIpAddress(ip) => write!(f, "Invalid IP address: {}", ip),
|
||||||
|
ConfigError::MissingRequiredField { host, field } => {
|
||||||
|
write!(f, "Missing required field '{}' for host '{}'", field, host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for ConfigError {}
|
||||||
|
|
||||||
|
pub struct RepoConfig {
|
||||||
|
pub name: String,
|
||||||
|
pub owner: String,
|
||||||
|
pub repo: String,
|
||||||
|
pub hostname: String,
|
||||||
|
pub user: String,
|
||||||
|
pub token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Config {
|
||||||
|
pub repos: HashMap<String, RepoConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_config<P: AsRef<Path>>(file_path: P) -> Result<Config, ConfigError> {
|
||||||
|
// Load the INI file
|
||||||
|
let conf = Ini::load_from_file(file_path).map_err(ConfigError::IniError)?;
|
||||||
|
|
||||||
|
let mut repos = HashMap::new();
|
||||||
|
// Process each section (except global)
|
||||||
|
for (section_name, properties) in conf.iter() {
|
||||||
|
if let Some(section_name) = section_name {
|
||||||
|
let host_info = parse_repo_section(section_name, properties)?;
|
||||||
|
|
||||||
|
repos.insert(section_name.to_string(), host_info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Config { repos })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_repo_section(
|
||||||
|
section_name: &str,
|
||||||
|
properties: &ini::Properties,
|
||||||
|
) -> Result<RepoConfig, ConfigError> {
|
||||||
|
let name = section_name.to_string();
|
||||||
|
let hostname = properties
|
||||||
|
.get("hostname")
|
||||||
|
.ok_or_else(|| ConfigError::MissingRequiredField {
|
||||||
|
host: name.clone(),
|
||||||
|
field: "hostname".to_string(),
|
||||||
|
})?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let owner = properties
|
||||||
|
.get("owner")
|
||||||
|
.ok_or_else(|| ConfigError::MissingRequiredField {
|
||||||
|
host: name.clone(),
|
||||||
|
field: "owner".to_string(),
|
||||||
|
})?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let repo = properties
|
||||||
|
.get("repo")
|
||||||
|
.ok_or_else(|| ConfigError::MissingRequiredField {
|
||||||
|
host: name.clone(),
|
||||||
|
field: "repo".to_string(),
|
||||||
|
})?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let user = properties
|
||||||
|
.get("user")
|
||||||
|
.ok_or_else(|| ConfigError::MissingRequiredField {
|
||||||
|
host: name.clone(),
|
||||||
|
field: "user".to_string(),
|
||||||
|
})?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let token = properties
|
||||||
|
.get("token")
|
||||||
|
.ok_or_else(|| ConfigError::MissingRequiredField {
|
||||||
|
host: name.clone(),
|
||||||
|
field: "token".to_string(),
|
||||||
|
})?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Ok(RepoConfig {
|
||||||
|
name,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
hostname,
|
||||||
|
user,
|
||||||
|
token,
|
||||||
|
})
|
||||||
|
}
|
124
src/main.rs
Normal file
124
src/main.rs
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use forgejo_api::Auth::Token;
|
||||||
|
use forgejo_api::Forgejo;
|
||||||
|
use forgejo_api::structs::CreateReleaseOption;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
pub mod config;
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[clap(
|
||||||
|
name = "forge-auto",
|
||||||
|
version = "1.0",
|
||||||
|
about = "Automate routine tasks in Forgejo"
|
||||||
|
)]
|
||||||
|
struct Cli {
|
||||||
|
#[clap(subcommand)]
|
||||||
|
command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
enum Commands {
|
||||||
|
/// A subcommand with its own subcommands
|
||||||
|
#[clap(subcommand)]
|
||||||
|
Release(ReleaseSubCommands),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
enum ReleaseSubCommands {
|
||||||
|
/// A sub-subcommand
|
||||||
|
NewRelease {
|
||||||
|
alias: String,
|
||||||
|
tag: String,
|
||||||
|
#[clap(long, short)]
|
||||||
|
target: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Another sub-subcommand
|
||||||
|
ListReleases {
|
||||||
|
#[clap(short, long)]
|
||||||
|
output: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_new_release(
|
||||||
|
config: &config::Config,
|
||||||
|
alias: &str,
|
||||||
|
tag: &str,
|
||||||
|
target: &Option<String>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
println!("Creating new release with alias: {}, tag: {}", alias, tag);
|
||||||
|
|
||||||
|
let repo = config.repos.get(alias).map_or_else(
|
||||||
|
|| Err(format!("No repository found with alias: {}", alias)),
|
||||||
|
|repo| {
|
||||||
|
println!("Using repository: {} owned by {}", repo.repo, repo.owner);
|
||||||
|
// Here you would typically call the Forgejo API to create a new release
|
||||||
|
// For example:
|
||||||
|
Ok(repo)
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let token = Token(&repo.token);
|
||||||
|
let url = Url::parse(&repo.hostname).map_err(|e| {
|
||||||
|
eprintln!("Failed to parse URL: {}", e);
|
||||||
|
e.to_string()
|
||||||
|
})?;
|
||||||
|
let forgejo = Forgejo::new(token, url).map_err(|e| {
|
||||||
|
eprintln!("Failed to create Forgejo client: {}", e);
|
||||||
|
e.to_string()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let option = CreateReleaseOption {
|
||||||
|
tag_name: tag.to_string(),
|
||||||
|
name: Some(format!("Release {}", tag)),
|
||||||
|
body: Some(format!("Release notes for {}", tag)),
|
||||||
|
hide_archive_links: Some(false),
|
||||||
|
target_commitish: Some(tag.to_string()),
|
||||||
|
draft: None,
|
||||||
|
prerelease: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
forgejo
|
||||||
|
.repo_create_release(&repo.owner, &repo.repo, option)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Failed to create release: {}", e);
|
||||||
|
e.to_string()
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_another_sub_sub_command(output: &str) {
|
||||||
|
println!("Executing AnotherSubSubCommand with output: {}", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_another_command(option: &str) {
|
||||||
|
println!("Executing AnotherCommand with option: {}", option);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let args = Cli::parse();
|
||||||
|
|
||||||
|
let config = config::load_config("config.ini").expect("Failed to load config");
|
||||||
|
|
||||||
|
match args.command {
|
||||||
|
Commands::Release(subcommand) => {
|
||||||
|
match subcommand {
|
||||||
|
ReleaseSubCommands::NewRelease { alias, tag, target } => {
|
||||||
|
handle_new_release(&config, &alias, &tag, &target).await?
|
||||||
|
}
|
||||||
|
ReleaseSubCommands::ListReleases { output } => {
|
||||||
|
handle_another_sub_sub_command(&output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Uncomment the following lines if you want to use the dispatch macro
|
||||||
|
// dispatch!(subcommand,
|
||||||
|
// SubCommands::SubSubCommand { input } => handle_sub_sub_command(&input),
|
||||||
|
// SubCommands::AnotherSubSubCommand { output } => handle_another_sub_sub_command(&output),
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue