Skip to content
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
4 changes: 1 addition & 3 deletions crates/diecut-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@ pub fn generate(options: GenerateOptions) -> Result<GeneratedProject> {
let source = resolve_source(&options.template)?;
let template_dir = match &source {
TemplateSource::Local(path) => path.clone(),
TemplateSource::Git { url, git_ref } => {
get_or_clone(url, git_ref.as_deref())?
}
TemplateSource::Git { url, git_ref } => get_or_clone(url, git_ref.as_deref())?,
};

// 2. Resolve template (auto-detect format, parse config)
Expand Down
53 changes: 26 additions & 27 deletions crates/diecut-core/src/template/cache.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::path::{Path, PathBuf};

use sha2::{Sha256, Digest};
use sha2::{Digest, Sha256};

use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -71,17 +71,10 @@ pub(crate) fn cache_key(url: &str, git_ref: Option<&str>) -> String {
let hash: String = digest.iter().take(8).map(|b| format!("{b:02x}")).collect();

// Sanitize components to prevent path traversal
let sanitize = |s: &str| -> String {
s.replace(['/', '\\'], "_").replace("..", "_")
};
let sanitize = |s: &str| -> String { s.replace(['/', '\\'], "_").replace("..", "_") };

// Build a human-readable prefix from the URL
let prefix = sanitize(
normalized
.rsplit('/')
.next()
.unwrap_or("template"),
);
let prefix = sanitize(normalized.rsplit('/').next().unwrap_or("template"));

match git_ref {
Some(r) => format!("{prefix}-{}-{hash}", sanitize(r)),
Expand Down Expand Up @@ -120,9 +113,10 @@ pub fn get_or_clone(url: &str, git_ref: Option<&str>) -> Result<PathBuf> {
git_ref: git_ref.map(String::from),
cached_at: unix_timestamp_secs(),
};
let metadata_toml = toml::to_string_pretty(&metadata).map_err(|e| DicecutError::CacheMetadata {
context: format!("serializing cache metadata: {e}"),
})?;
let metadata_toml =
toml::to_string_pretty(&metadata).map_err(|e| DicecutError::CacheMetadata {
context: format!("serializing cache metadata: {e}"),
})?;
std::fs::write(tmp_dir.path().join(CACHE_METADATA_FILE), metadata_toml).map_err(|e| {
DicecutError::Io {
context: "writing cache metadata".into(),
Expand All @@ -143,9 +137,7 @@ pub fn get_or_clone(url: &str, git_ref: Option<&str>) -> Result<PathBuf> {
std::fs::rename(tmp_dir.path(), &cached_path).or_else(|rename_err| {
// rename can fail across filesystems; fall back to copy + delete
copy_dir_all(tmp_dir.path(), &cached_path).map_err(|e| DicecutError::Io {
context: format!(
"copying cloned template to cache (rename failed: {rename_err}): {e}"
),
context: format!("copying cloned template to cache (rename failed: {rename_err}): {e}"),
source: std::io::Error::other(e.to_string()),
})?;
Ok(())
Expand Down Expand Up @@ -187,20 +179,18 @@ pub fn list_cached() -> Result<Vec<CachedTemplate>> {
continue;
}

let metadata_str = std::fs::read_to_string(&metadata_path).map_err(|e| DicecutError::Io {
context: format!("reading cache metadata {}", metadata_path.display()),
source: e,
})?;
let metadata_str =
std::fs::read_to_string(&metadata_path).map_err(|e| DicecutError::Io {
context: format!("reading cache metadata {}", metadata_path.display()),
source: e,
})?;

let metadata: CacheMetadata =
toml::from_str(&metadata_str).map_err(|e| DicecutError::CacheMetadata {
context: format!("parsing cache metadata: {e}"),
})?;

let key = entry
.file_name()
.to_string_lossy()
.into_owned();
let key = entry.file_name().to_string_lossy().into_owned();

entries.push(CachedTemplate {
key,
Expand Down Expand Up @@ -364,9 +354,18 @@ mod tests {

#[test]
fn normalize_url_strips_trailing_git_and_slash() {
assert_eq!(normalize_url("https://github.com/user/repo.git"), "https://github.com/user/repo");
assert_eq!(normalize_url("https://github.com/user/repo/"), "https://github.com/user/repo");
assert_eq!(normalize_url("https://github.com/user/repo"), "https://github.com/user/repo");
assert_eq!(
normalize_url("https://github.com/user/repo.git"),
"https://github.com/user/repo"
);
assert_eq!(
normalize_url("https://github.com/user/repo/"),
"https://github.com/user/repo"
);
assert_eq!(
normalize_url("https://github.com/user/repo"),
"https://github.com/user/repo"
);
}

#[test]
Expand Down
28 changes: 13 additions & 15 deletions crates/diecut-core/src/template/clone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,20 @@ pub fn clone_template(url: &str, git_ref: Option<&str>) -> Result<tempfile::Temp
})?;

// Use prepare_clone (with worktree) so we get a checked-out working copy
let mut prepare = gix::prepare_clone(url, tmp_dir.path()).map_err(|e| {
DicecutError::GitClone {
let mut prepare =
gix::prepare_clone(url, tmp_dir.path()).map_err(|e| DicecutError::GitClone {
url: url.to_string(),
reason: e.to_string(),
}
})?;
})?;

// If a specific ref is requested, configure it before fetching
if let Some(ref_name) = git_ref {
prepare = prepare.with_ref_name(Some(ref_name)).map_err(|e| {
DicecutError::GitCheckout {
prepare = prepare
.with_ref_name(Some(ref_name))
.map_err(|e| DicecutError::GitCheckout {
git_ref: ref_name.to_string(),
reason: e.to_string(),
}
})?;
})?;
}

// Fetch and prepare for checkout
Expand All @@ -54,13 +53,12 @@ pub fn clone_template(url: &str, git_ref: Option<&str>) -> Result<tempfile::Temp
})?;

// Checkout the main worktree
let (_repo, _outcome) =
checkout
.main_worktree(gix::progress::Discard, &gix::interrupt::IS_INTERRUPTED)
.map_err(|e| DicecutError::GitClone {
url: url.to_string(),
reason: format!("worktree checkout failed: {e}"),
})?;
let (_repo, _outcome) = checkout
.main_worktree(gix::progress::Discard, &gix::interrupt::IS_INTERRUPTED)
.map_err(|e| DicecutError::GitClone {
url: url.to_string(),
reason: format!("worktree checkout failed: {e}"),
})?;

Ok(tmp_dir)
}
Expand Down
2 changes: 1 addition & 1 deletion crates/diecut-core/src/template/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ pub mod cache;
pub mod clone;
pub mod source;

pub use cache::{get_or_clone, list_cached, clear_cache, CachedTemplate};
pub use cache::{clear_cache, get_or_clone, list_cached, CachedTemplate};
pub use clone::clone_template;
pub use source::{resolve_source, resolve_source_with_ref, TemplateSource};
Loading