Initial commit

This commit is contained in:
2025-11-04 15:40:02 +01:00
commit 3658b090e0
42 changed files with 5429 additions and 0 deletions

21
rust/Cargo.toml Normal file
View File

@@ -0,0 +1,21 @@
[package]
name = "gitreposetup"
version = "0.1.0"
edition = "2021"
description = "A tool to initialize and configure git repositories with licenses and workflows"
[[bin]]
name = "gitreposetup"
path = "src/main.rs"
[[bin]]
name = "grs"
path = "src/main.rs"
[dependencies]
clap = { version = "4.5", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.9"
git2 = "0.19"
html-escape = "0.2"
chrono = "0.4"

335
rust/src/main.rs Normal file
View File

@@ -0,0 +1,335 @@
use clap::{Parser, ValueEnum};
use serde::{Deserialize, Serialize};
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
#[derive(Debug, Clone, ValueEnum)]
enum LicenseType {
#[value(name = "MIT")]
MIT,
#[value(name = "GPLv3")]
GPLv3,
#[value(name = "AGPLv3")]
AGPLv3,
#[value(name = "Unlicense")]
Unlicense,
}
impl LicenseType {
fn filename(&self) -> &'static str {
match self {
LicenseType::MIT => "MIT.txt",
LicenseType::GPLv3 => "GPL-3.0.txt",
LicenseType::AGPLv3 => "AGPL-3.0.txt",
LicenseType::Unlicense => "Unlicense.txt",
}
}
}
#[derive(Debug, Clone, ValueEnum)]
enum DeployType {
#[value(name = "docker")]
Docker,
#[value(name = "pypi")]
PyPI,
#[value(name = "cargo")]
Cargo,
#[value(name = "go")]
Go,
}
#[derive(Debug, Parser)]
#[command(name = "gitreposetup")]
#[command(about = "Initialize and configure git repositories with licenses and workflows")]
struct Args {
#[arg(long, help = "Repository owner/organization name")]
owner: Option<String>,
#[arg(long, help = "Repository name")]
name: Option<String>,
#[arg(long, value_enum, help = "License type")]
license: Option<LicenseType>,
#[arg(long, help = "Development branch name (default: dev)")]
develop_branch_name: Option<String>,
#[arg(long, help = "Use default .gitignore")]
default_gitignore: Option<bool>,
#[arg(long, help = "Git remote URL template")]
default_git_url: Option<String>,
#[arg(long, help = "Force overwrite existing LICENSE")]
force: bool,
#[arg(long, value_enum, help = "Deployment type for Gitea workflows")]
deploy_type: Option<DeployType>,
}
#[derive(Debug, Serialize, Deserialize)]
struct Config {
owner: String,
name: String,
license: String,
develop_branch: String,
default_gitignore: bool,
default_git_url: String,
}
impl Default for Config {
fn default() -> Self {
Config {
owner: String::new(),
name: String::new(),
license: "MIT".to_string(),
develop_branch: "dev".to_string(),
default_gitignore: true,
default_git_url: "https://git.theprivateserver.de/{owner}/{repo}.git".to_string(),
}
}
}
// Embedded licenses
const MIT_LICENSE: &str = include_str!("../../licenses/MIT.txt");
const GPL_LICENSE: &str = include_str!("../../licenses/GPL-3.0.txt");
const AGPL_LICENSE: &str = include_str!("../../licenses/AGPL-3.0.txt");
const UNLICENSE: &str = include_str!("../../licenses/Unlicense.txt");
// Embedded .gitignore
const DEFAULT_GITIGNORE: &str = include_str!("../../.gitignore");
fn get_config_path() -> PathBuf {
let home = if cfg!(windows) {
std::env::var("USERPROFILE").unwrap_or_else(|_| ".".to_string())
} else {
std::env::var("HOME").unwrap_or_else(|_| ".".to_string())
};
PathBuf::from(home).join(".config").join("GMS").join(".config.yaml")
}
fn load_config() -> Config {
let config_path = get_config_path();
if config_path.exists() {
let content = fs::read_to_string(&config_path).unwrap_or_default();
serde_yaml::from_str(&content).unwrap_or_default()
} else {
let config = Config::default();
// Create config directory
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent).ok();
}
// Write default config
if let Ok(yaml) = serde_yaml::to_string(&config) {
fs::write(&config_path, yaml).ok();
println!("Created default config at: {}", config_path.display());
}
config
}
}
fn run_git_command(args: &[&str], check: bool) -> Option<String> {
let output = Command::new("git")
.args(args)
.output();
match output {
Ok(output) if output.status.success() || !check => {
Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
_ => None,
}
}
fn is_git_repo() -> bool {
run_git_command(&["rev-parse", "--git-dir"], false).is_some()
}
fn get_git_user_name() -> Option<String> {
run_git_command(&["config", "user.name"], false)
}
fn decode_html_entities(text: &str) -> String {
html_escape::decode_html_entities(text).to_string()
}
fn get_license_content(license_type: &LicenseType, owner: &str) -> String {
let license_text = match license_type {
LicenseType::MIT => MIT_LICENSE,
LicenseType::GPLv3 => GPL_LICENSE,
LicenseType::AGPLv3 => AGPL_LICENSE,
LicenseType::Unlicense => UNLICENSE,
};
let license_text = decode_html_entities(license_text);
// Get fullname from git config or use owner
let fullname = get_git_user_name().unwrap_or_else(|| owner.to_string());
let current_year = chrono::Local::now().format("%Y").to_string();
// Substitute placeholders
license_text
.replace("{year}", &current_year)
.replace("{fullname}", &fullname)
}
fn remove_all_remotes() {
if let Some(remotes) = run_git_command(&["remote"], false) {
for remote in remotes.lines() {
let remote = remote.trim();
if !remote.is_empty() {
run_git_command(&["remote", "remove", remote], false);
}
}
}
}
fn setup_git_repo(config: &Config, force_license: bool) -> Result<(), String> {
// Initialize repo if needed
if !is_git_repo() {
run_git_command(&["init"], true);
println!("Initialized new git repository");
}
// Ensure main branch exists
let branches = run_git_command(&["branch", "--list"], false).unwrap_or_default();
if !branches.contains("main") {
if branches.trim().is_empty() {
run_git_command(&["checkout", "-b", "main"], true);
} else {
run_git_command(&["branch", "-M", "main"], true);
}
} else {
run_git_command(&["checkout", "main"], false);
}
// Set up remote
let remote_url = config
.default_git_url
.replace("{owner}", &config.owner)
.replace("{repo}", &config.name);
// Remove all existing remotes and add new origin
remove_all_remotes();
run_git_command(&["remote", "add", "origin", &remote_url], true);
println!("Set remote 'origin' to: {}", remote_url);
// Add LICENSE if missing or forced
let license_path = PathBuf::from("LICENSE");
if !license_path.exists() || force_license {
let license_type = match config.license.as_str() {
"GPLv3" => LicenseType::GPLv3,
"AGPLv3" => LicenseType::AGPLv3,
"Unlicense" => LicenseType::Unlicense,
_ => LicenseType::MIT,
};
let license_content = get_license_content(&license_type, &config.owner);
fs::write(&license_path, license_content).map_err(|e| e.to_string())?;
println!("Added LICENSE: {}", config.license);
}
// Add/overwrite .gitignore if enabled
if config.default_gitignore {
fs::write(".gitignore", DEFAULT_GITIGNORE).map_err(|e| e.to_string())?;
println!("Added/updated .gitignore");
}
// Stage changes
run_git_command(&["add", "."], true);
// Commit if there are staged changes
let status = run_git_command(&["status", "--porcelain"], false).unwrap_or_default();
if !status.is_empty() {
run_git_command(&["commit", "-m", "Initial commit"], true);
println!("Created initial commit");
}
// Create and checkout dev branch
let dev_branch = &config.develop_branch;
run_git_command(&["checkout", "-b", dev_branch], false);
println!("Created and switched to branch: {}", dev_branch);
// Try to push to remote
let push_main = run_git_command(&["push", "-u", "origin", "main"], false);
let push_dev = run_git_command(&["push", "-u", "origin", dev_branch], false);
if push_main.is_some() && push_dev.is_some() {
println!("Pushed to remote successfully");
} else {
println!("⚠️ Warning: Could not push to remote. Network may be unavailable or remote not accessible.");
println!(" Repository configured locally. Push manually when ready.");
}
Ok(())
}
fn setup_gitea_workflows(deploy_type: &DeployType) {
let gitea_dir = PathBuf::from(".gitea");
let workflows_dir = gitea_dir.join("workflows");
// Create directories
fs::create_dir_all(&workflows_dir).ok();
// TODO: Copy/create workflow files based on deploy_type
println!("Set up Gitea workflows for: {:?}", deploy_type);
println!("⚠️ Note: Ensure required secrets are configured in Gitea user/org settings:");
println!(" - GITEA_TOKEN, TOKEN, REGISTRY, DOCKER_USERNAME");
if matches!(deploy_type, DeployType::Docker) {
println!("⚠️ Warning: Docker workflows require a Dockerfile in the repository root.");
}
}
fn main() {
let args = Args::parse();
// Load config
let mut config = load_config();
// Override config with CLI arguments
if let Some(owner) = args.owner {
config.owner = owner;
}
if let Some(name) = args.name {
config.name = name;
}
if let Some(license) = args.license {
config.license = format!("{:?}", license);
}
if let Some(develop_branch) = args.develop_branch_name {
config.develop_branch = develop_branch;
}
if let Some(default_gitignore) = args.default_gitignore {
config.default_gitignore = default_gitignore;
}
if let Some(default_git_url) = args.default_git_url {
config.default_git_url = default_git_url;
}
// Validate required fields
if config.owner.is_empty() || config.name.is_empty() {
eprintln!("Error: --owner and --name are required (or set in config file)");
std::process::exit(1);
}
// Setup git repository
if let Err(e) = setup_git_repo(&config, args.force) {
eprintln!("Error setting up git repository: {}", e);
std::process::exit(1);
}
// Setup workflows if requested
if let Some(deploy_type) = args.deploy_type {
setup_gitea_workflows(&deploy_type);
}
println!("\n✅ Repository setup complete!");
}