Skip to content

Commit

Permalink
scaffolding: add scaffolding feature (#161)
Browse files Browse the repository at this point in the history
  • Loading branch information
fioncat authored Dec 23, 2024
1 parent 1df4dfa commit 5d0b750
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 24 deletions.
2 changes: 1 addition & 1 deletion src/cmd/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ impl Run for CheckArgs {

for repo in to_remove {
let path = repo.get_path(cfg);
utils::remove_dir_recursively(path)?;
utils::remove_dir_recursively(path, true)?;
db.remove(repo);
}

Expand Down
2 changes: 1 addition & 1 deletion src/cmd/clean.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ impl CleanArgs {
}

for dir in dirs {
utils::remove_dir_recursively(dir)?;
utils::remove_dir_recursively(dir, true)?;
}

Ok(())
Expand Down
123 changes: 107 additions & 16 deletions src/cmd/home.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
use std::fs;
use std::io;
use std::path::Path;
use std::path::PathBuf;

use anyhow::{Context, Result};
use clap::Args;

use crate::batch::Task;
use crate::cmd::{Completion, Run};
use crate::cmd::{Completion, CompletionResult, Run};
use crate::config::Config;
use crate::error;
use crate::exec::Cmd;
use crate::info;
use crate::repo::database::{Database, SelectOptions, Selector};
use crate::repo::detect::labels::DetectLabels;
use crate::repo::Repo;
Expand Down Expand Up @@ -41,6 +42,10 @@ pub struct HomeArgs {
#[clap(short, long)]
pub thin: bool,

/// Use a scaffolding to create the repo.
#[clap(short, long)]
pub bootstrap: Option<String>,

/// Append these labels to the database.
#[clap(short, long)]
pub labels: Option<String>,
Expand Down Expand Up @@ -73,7 +78,13 @@ impl Run for HomeArgs {
match fs::read_dir(&path) {
Ok(_) => {}
Err(err) if err.kind() == io::ErrorKind::NotFound => {
self.create_dir(cfg, &repo, &path)?;
let result = self.create_dir(cfg, &repo, &path);
if result.is_err() {
if let Err(err) = utils::remove_dir_recursively(path.clone(), false) {
error!("Remove garbage path '{}' failed: {}", path.display(), err);
}
return result;
}
}
Err(err) => {
return Err(err).with_context(|| format!("read repo directory {}", path.display()));
Expand All @@ -98,26 +109,34 @@ impl Run for HomeArgs {
}

impl HomeArgs {
fn create_dir(&self, cfg: &Config, repo: &Repo, path: &PathBuf) -> Result<()> {
if repo.remote_cfg.clone.is_some() {
return self.clone(repo, path);
fn create_dir(&self, cfg: &Config, repo: &Repo, path: &Path) -> Result<()> {
if let Some(ref name) = self.bootstrap {
self.clone_from_scaffolding(name, repo, path, cfg)
} else if repo.remote_cfg.clone.is_some() {
self.clone(repo, path)
} else {
self.create_local(path)
}?;

if let Some(owner) = repo.remote_cfg.owners.get(repo.owner.as_ref()) {
if let Some(on_create) = &owner.on_create {
for wf_name in on_create.iter() {
let wf = Workflow::load(wf_name, cfg, repo)?;
wf.run()?;
}
}
}

Ok(())
}

fn create_local(&self, path: &Path) -> Result<()> {
fs::create_dir_all(path)
.with_context(|| format!("create repo directory {}", path.display()))?;
let path = format!("{}", path.display());
Cmd::git(&["-C", path.as_str(), "init"])
.with_display("Git init")
.execute()?;
if let Some(owner) = repo.remote_cfg.owners.get(repo.owner.as_ref()) {
if let Some(workflow_names) = &owner.on_create {
for workflow_name in workflow_names.iter() {
let wf = Workflow::load(workflow_name, cfg, repo)?;
wf.run()?;
}
}
}

Ok(())
}

Expand All @@ -133,6 +152,64 @@ impl HomeArgs {
.with_display(format!("Clone {}", repo.name_with_remote()))
.execute()?;

self.init_repo_user(repo, path.as_ref())?;
Ok(())
}

fn clone_from_scaffolding(
&self,
name: &str,
repo: &Repo,
path: &Path,
cfg: &Config,
) -> Result<()> {
let scaf_conf = cfg.get_scaffolding(name)?;
let mut wfs = Vec::new();
if !scaf_conf.exec.is_empty() {
for wf_name in scaf_conf.exec.iter() {
let wf = Workflow::load(wf_name, cfg, repo)?;
wfs.push(wf);
}
}

Cmd::git(&[
"clone",
// The scaffolding repo's git info will be soon deleted, so its clone will always
// be shallow since we don't need the git history at all.
"--depth",
"1",
scaf_conf.clone.as_ref(),
path.to_str().unwrap(),
])
.with_display(format!("Clone scaffolding '{name}'"))
.execute()?;

for wf in wfs.iter() {
wf.run()?;
}

info!("Remove scaffolding git info");
let git_info_path = path.join(".git");
fs::remove_dir_all(git_info_path)?;

let path = format!("{}", path.display());
Cmd::git(&["-C", path.as_str(), "init"])
.with_display("Git init")
.execute()?;

if repo.remote_cfg.clone.is_some() {
self.init_repo_user(repo, path.as_ref())?;
let url = repo.clone_url();
Cmd::git(&["-C", path.as_str(), "remote", "add", "origin", url.as_str()])
.with_display(format!("Set remote origin url to '{}'", url))
.execute()?;
}

Ok(())
}

fn init_repo_user(&self, repo: &Repo, path: &Path) -> Result<()> {
let path = format!("{}", path.display());
if let Some(user) = &repo.remote_cfg.user {
Cmd::git(&["-C", path.as_str(), "config", "user.name", user.as_str()])
.with_display(format!("Set user to {}", user))
Expand All @@ -149,7 +226,21 @@ impl HomeArgs {
pub fn completion() -> Completion {
Completion {
args: Completion::repo_args,
flags: Some(Completion::labels),
flags: Some(|cfg, flag, to_complete| match flag {
'l' => Completion::labels_flag(cfg, to_complete),
'b' => Self::complete_bootstrap(cfg, to_complete),
_ => Ok(None),
}),
}
}

fn complete_bootstrap(cfg: &Config, to_complete: &str) -> Result<Option<CompletionResult>> {
let mut items = Vec::with_capacity(cfg.scaffoldings.len());
for name in cfg.scaffoldings.keys() {
if name.starts_with(to_complete) {
items.push(name.clone());
}
}
Ok(Some(CompletionResult::from(items)))
}
}
4 changes: 2 additions & 2 deletions src/cmd/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ impl RemoveArgs {
confirm!("Do you want to remove repo {}", repo.name_with_remote());

let path = repo.get_path(cfg);
utils::remove_dir_recursively(path)?;
utils::remove_dir_recursively(path, true)?;

db.remove(repo.update());

Expand All @@ -93,7 +93,7 @@ impl RemoveArgs {
let mut update_repos = Vec::with_capacity(repos.len());
for repo in repos {
let path = repo.get_path(cfg);
utils::remove_dir_recursively(path)?;
utils::remove_dir_recursively(path, true)?;
update_repos.push(repo.update());
}
for repo in update_repos {
Expand Down
50 changes: 50 additions & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ pub struct Config {
#[serde(skip)]
pub workflows: HashMap<String, WorkflowConfig>,

/// Scaffolding configuration. Scaffolding is a special mechanism for creating
/// repositories. It uses a template repository to derive a new repository. The
/// specific derivation process involves first cloning the scaffolding repository,
/// then executing the initialization script, and finally deleting the `.git` of
/// the scaffolding project and reinitializing it with `git init`.
#[serde(skip)]
pub scaffoldings: HashMap<String, ScaffoldingConfig>,

#[serde(skip)]
current_dir: Option<PathBuf>,

Expand Down Expand Up @@ -329,6 +337,16 @@ pub enum ProviderType {
Gitlab,
}

/// The configuration for scaffolding.
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct ScaffoldingConfig {
/// The clone url of scaffolding repo.
pub clone: String,

/// The workflow to execute after cloning the scaffolding repo.
pub exec: Vec<String>,
}

impl RemoteConfig {
pub fn get_name(&self) -> &str {
self.name.as_ref().unwrap().as_str()
Expand Down Expand Up @@ -455,8 +473,12 @@ impl Config {
let workflows_dir = root.join("workflows");
let workflows = Self::load_workflows(&workflows_dir)?;

let scaffoldings_dir = root.join("scaffoldings");
let scaffoldings = Self::load_scaffoldings(&scaffoldings_dir)?;

cfg.remotes = remotes;
cfg.workflows = workflows;
cfg.scaffoldings = scaffoldings;

cfg.validate().context("validate config content")?;

Expand All @@ -471,6 +493,10 @@ impl Config {
Self::load_config_items(dir)
}

pub fn load_scaffoldings(dir: &Path) -> Result<HashMap<String, ScaffoldingConfig>> {
Self::load_config_items(dir)
}

fn load_config_items<T: DeserializeOwned>(dir: &Path) -> Result<HashMap<String, T>> {
let dir_read = match fs::read_dir(dir) {
Ok(read) => read,
Expand Down Expand Up @@ -530,6 +556,7 @@ impl Config {
remotes: HashMap::new(),
release: defaults::release(),
workflows: defaults::empty_map(),
scaffoldings: defaults::empty_map(),
detect_ignores: defaults::empty_vec(),
current_dir: None,
now: None,
Expand Down Expand Up @@ -579,6 +606,21 @@ impl Config {
remote.name = Some(name.clone());
}

for (name, scaf) in self.scaffoldings.iter() {
if scaf.clone.is_empty() {
bail!("scaffolding '{}' clone url is empty", name);
}
for wf_name in scaf.exec.iter() {
if !self.workflows.contains_key(wf_name) {
bail!(
"scaffolding '{}' exec workflow '{}' not found",
name,
wf_name
);
}
}
}

let current_dir = env::current_dir().context("get current work directory")?;
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
Expand Down Expand Up @@ -721,6 +763,14 @@ impl Config {
Ok(())
}

pub fn get_scaffolding(&self, name: impl AsRef<str>) -> Result<Cow<'_, ScaffoldingConfig>> {
let scaffolding = match self.scaffoldings.get(name.as_ref()) {
Some(scaffolding) => scaffolding,
None => bail!("could not find scaffolding '{}'", name.as_ref()),
};
Ok(Cow::Borrowed(scaffolding))
}

fn parse_patterns(raw: &[String]) -> Result<Vec<GlobPattern>> {
let mut patterns = Vec::with_capacity(raw.len());
for str in raw.iter() {
Expand Down
12 changes: 8 additions & 4 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,13 +349,15 @@ pub fn plural_full<T>(vec: &[T], name: &str, plural: &str) -> String {

/// Remove a directory, recursively deleting until reaching a non-empty parent
/// directory.
pub fn remove_dir_recursively(path: PathBuf) -> Result<()> {
pub fn remove_dir_recursively(path: PathBuf, display: bool) -> Result<()> {
match fs::read_dir(&path) {
Ok(_) => {}
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(err) => return Err(err).with_context(|| format!("read repo dir '{}'", path.display())),
}
info!("Remove dir {}", path.display());
if display {
info!("Remove dir {}", path.display());
}
fs::remove_dir_all(&path).context("remove directory")?;

let dir = path.parent();
Expand All @@ -370,7 +372,9 @@ pub fn remove_dir_recursively(path: PathBuf) -> Result<()> {
if count > 0 {
return Ok(());
}
info!("Remove dir {}", dir.display());
if display {
info!("Remove dir {}", dir.display());
}
fs::remove_dir(dir).context("remove directory")?;
match dir.parent() {
Some(parent) => dir = parent,
Expand Down Expand Up @@ -469,7 +473,7 @@ mod utils_tests {
fn test_remove_dir_recursively() {
const PATH: &str = "/tmp/test-roxide/sub01/sub02/sub03";
fs::create_dir_all(PATH).unwrap();
remove_dir_recursively(PathBuf::from(PATH)).unwrap();
remove_dir_recursively(PathBuf::from(PATH), false).unwrap();

match fs::read_dir(PATH) {
Ok(_) => panic!("Expect path {PATH} be deleted, but it is still exists"),
Expand Down

0 comments on commit 5d0b750

Please sign in to comment.