Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

scaffolding: add scaffolding feature #161

Merged
merged 1 commit into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading