Initial commit
This commit is contained in:
21
rust/Cargo.toml
Normal file
21
rust/Cargo.toml
Normal 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
335
rust/src/main.rs
Normal 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}", ¤t_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!");
|
||||
}
|
||||
Reference in New Issue
Block a user