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
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Changelog

## [Unreleased]

### Changed

- **Breaking:** Built-in shortcodes (`gh:`, `gl:`, `cb:`) now default to SSH
URLs instead of HTTPS. The `gh config get git_protocol` detection has been
removed — behavior is now consistent across all three vendors and independent
of the `gh` CLI.

### Added

- `--protocol <ssh|https>` flag on `diecut new` for per-invocation override of
shortcode protocol.
- `DIECUT_GIT_PROTOCOL` environment variable for persistent shortcode protocol
preference.
- Dry-run output now prints the resolved clone URL (or local path) before any
clone is attempted.

## [0.3.5](https://github.com/raiderrobert/diecut/compare/v0.3.4...v0.3.5) (2026-03-15)


Expand Down
42 changes: 42 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ indexmap = { version = "2.11.4", features = ["serde"] }
[dev-dependencies]
rstest = "0.23"
criterion = "0.5"
serial_test = "3"

[[bench]]
name = "benchmarks"
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ diecut list

Example templates: [diecut-templates](https://github.com/raiderrobert/diecut-templates)

### Protocol

Built-in shortcodes resolve to SSH URLs by default (e.g., `gh:user/repo` →
`git@github.com:user/repo.git`). To use HTTPS instead, pass `--protocol https`
or set `DIECUT_GIT_PROTOCOL=https` in your shell environment.

## Documentation

Full documentation: **[diecut.dev](https://diecut.dev/)**
Expand Down
2 changes: 2 additions & 0 deletions docs/src/content/docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ diecut new <TEMPLATE> [OPTIONS]
| `--no-hooks` | `false` | Skip running hooks |
| `--dry-run` | `false` | Show what would be generated without writing files |
| `-v, --verbose` | `false` | Show file contents (with `--dry-run`) or detailed output |
| `--protocol <ssh\|https>` | `ssh` | Protocol for expanding built-in shortcodes (`gh:`, `gl:`, `cb:`) |

### Examples

Expand All @@ -51,6 +52,7 @@ diecut new ./my-template --dry-run --verbose
- The `--data` flag can be repeated to set multiple variables.
- When `--defaults` is set, any variable without a default value causes an error.
- Subpaths let you point to a template inside a larger repo (e.g., `gh:user/templates/python-pkg`).
- `DIECUT_GIT_PROTOCOL` sets the default shortcode protocol persistently (`ssh` or `https`). Overridden per-invocation by `--protocol`.

---

Expand Down
6 changes: 6 additions & 0 deletions docs/src/content/docs/using-templates/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ diecut pulls templates from local paths or any Git host.

Git templates are cached at `~/.cache/diecut/templates/`. Override with `DIECUT_CACHE_DIR`.

## Protocol

Built-in shortcodes (`gh:`, `gl:`, `cb:`) resolve to SSH URLs by default (e.g.,
`gh:user/repo` → `git@github.com:user/repo.git`). To use HTTPS instead, pass
`--protocol https` or set `DIECUT_GIT_PROTOCOL=https` in your shell environment.

## Multi-template repos (subpaths)

A single repo can hold multiple templates in subdirectories. Add the subdirectory path after the repo:
Expand Down
49 changes: 49 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use clap::{Parser, Subcommand};
use diecut::template::GitProtocol;

#[derive(Parser)]
#[command(
Expand Down Expand Up @@ -45,8 +46,56 @@ pub enum Commands {
/// Show file contents (with --dry-run) or detailed output
#[arg(short, long)]
verbose: bool,

/// Protocol for expanding shortcodes (ssh or https).
/// Defaults to ssh. Override with DIECUT_GIT_PROTOCOL env var.
#[arg(long, value_enum)]
protocol: Option<GitProtocol>,
},

/// List cached templates
List,
}

#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
use diecut::template::GitProtocol;

#[test]
fn parses_new_without_protocol() {
let cli = Cli::parse_from(["diecut", "new", "gh:user/repo"]);
if let Commands::New { protocol, .. } = cli.command {
assert!(protocol.is_none());
} else {
panic!("expected New");
}
}

#[test]
fn parses_new_with_protocol_ssh() {
let cli = Cli::parse_from(["diecut", "new", "gh:user/repo", "--protocol", "ssh"]);
if let Commands::New { protocol, .. } = cli.command {
assert_eq!(protocol, Some(GitProtocol::Ssh));
} else {
panic!("expected New");
}
}

#[test]
fn parses_new_with_protocol_https() {
let cli = Cli::parse_from(["diecut", "new", "gh:user/repo", "--protocol", "https"]);
if let Commands::New { protocol, .. } = cli.command {
assert_eq!(protocol, Some(GitProtocol::Https));
} else {
panic!("expected New");
}
}

#[test]
fn rejects_invalid_protocol() {
let result = Cli::try_parse_from(["diecut", "new", "gh:user/repo", "--protocol", "ftp"]);
assert!(result.is_err());
}
}
19 changes: 19 additions & 0 deletions src/commands/new.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use console::style;
use diecut::template::{
format_resolved_source, resolve_git_protocol, resolve_source, GitProtocol, ResolveOptions,
};
use diecut::GenerateOptions;
use miette::Result;

Expand All @@ -12,6 +15,7 @@ pub fn run(
no_hooks: bool,
dry_run: bool,
verbose: bool,
protocol: Option<GitProtocol>,
) -> Result<()> {
let data_pairs: Vec<(String, String)> = data
.into_iter()
Expand All @@ -23,13 +27,28 @@ pub fn run(
})
.collect();

let resolved_protocol = resolve_git_protocol(protocol)?;

if dry_run {
// Resolve the source first so the URL is visible even if clone fails.
let source = resolve_source(
&template,
ResolveOptions {
protocol: resolved_protocol,
..Default::default()
},
)?;
println!("{}", format_resolved_source(&source));
}

let options = GenerateOptions {
template,
output,
data: data_pairs,
defaults,
overwrite,
no_hooks,
protocol: resolved_protocol,
};

if dry_run {
Expand Down
7 changes: 7 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ pub enum DicecutError {
source: tera::Error,
},

#[error("Invalid git protocol value '{value}' in {config_key}")]
#[diagnostic(help("Expected 'ssh' or 'https'"))]
InvalidProtocol {
value: String,
config_key: &'static str,
},

#[error("Invalid template abbreviation: {input}")]
#[diagnostic(help("Supported abbreviations: gh:user/repo, gl:user/repo, cb:user/repo"))]
InvalidAbbreviation { input: String },
Expand Down
18 changes: 16 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use crate::answers::TemplateOrigin;
use crate::error::{DicecutError, Result};
use crate::prompt::{collect_variables, PromptOptions};
use crate::render::{build_context, execute_plan, plan_render, GeneratedProject, GenerationPlan};
use crate::template::{get_or_clone, resolve_source, TemplateSource};
use crate::template::{get_or_clone, resolve_source, GitProtocol, ResolveOptions, TemplateSource};

pub struct GenerateOptions {
pub template: String,
Expand All @@ -27,6 +27,7 @@ pub struct GenerateOptions {
pub defaults: bool,
pub overwrite: bool,
pub no_hooks: bool,
pub protocol: GitProtocol,
}

/// Everything needed to execute a generation that has been planned but not yet written.
Expand All @@ -44,7 +45,13 @@ pub struct FullGenerationPlan {
/// This performs all preparation (template resolution, variable collection, pre-generate
/// hooks, and rendering) but does **not** write any files to disk.
pub fn plan_generation(options: GenerateOptions) -> Result<FullGenerationPlan> {
let source = resolve_source(&options.template)?;
let source = resolve_source(
&options.template,
ResolveOptions {
protocol: options.protocol,
..Default::default()
},
)?;
let (template_dir, origin) = match &source {
TemplateSource::Local(path) => (path.clone(), TemplateOrigin::Local),
TemplateSource::Git {
Expand Down Expand Up @@ -222,6 +229,7 @@ default = "my-project"
defaults: false,
overwrite: false,
no_hooks: true,
protocol: GitProtocol::default(),
};

let plan = plan_generation(options).unwrap();
Expand All @@ -240,6 +248,7 @@ default = "my-project"
defaults: true,
overwrite: false,
no_hooks: true,
protocol: GitProtocol::default(),
};

let result = plan_generation(options);
Expand Down Expand Up @@ -267,6 +276,7 @@ default = "my-project"
defaults: true,
overwrite,
no_hooks: true,
protocol: GitProtocol::default(),
};

let result = plan_generation(options);
Expand Down Expand Up @@ -294,6 +304,7 @@ default = "my-project"
defaults: false,
overwrite: false,
no_hooks: true,
protocol: GitProtocol::default(),
};

let plan = plan_generation(options).unwrap();
Expand Down Expand Up @@ -326,6 +337,7 @@ default = "my-project"
defaults: false,
overwrite: true,
no_hooks: true,
protocol: GitProtocol::default(),
};

let plan = plan_generation(options).unwrap();
Expand Down Expand Up @@ -368,6 +380,7 @@ default = "test"
defaults: true,
overwrite: true,
no_hooks: true,
protocol: GitProtocol::default(),
};

let plan = plan_generation(options).unwrap();
Expand All @@ -394,6 +407,7 @@ default = "test"
defaults: false,
overwrite: true,
no_hooks: true,
protocol: GitProtocol::default(),
};

let result = generate(options).unwrap();
Expand Down
3 changes: 2 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ fn main() -> miette::Result<()> {
no_hooks,
dry_run,
verbose,
protocol,
} => commands::new::run(
template, output, data, defaults, overwrite, no_hooks, dry_run, verbose,
template, output, data, defaults, overwrite, no_hooks, dry_run, verbose, protocol,
),
Commands::List => commands::list::run(),
}
Expand Down
5 changes: 4 additions & 1 deletion src/template/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ pub mod source;

pub use cache::{clear_cache, get_or_clone, list_cached, CacheMetadata, CachedTemplate};
pub use clone::{clone_template, CloneResult};
pub use source::{resolve_source, resolve_source_full, resolve_source_with_ref, TemplateSource};
pub use source::{
format_resolved_source, resolve_git_protocol, resolve_source, GitProtocol, ResolveOptions,
TemplateSource,
};
Loading
Loading