Skip to content

Commit b3b1692

Browse files
authored
Merge pull request #1085 from itowlson/templates-track-source
Record where templates were installed from
2 parents 31ead45 + 41ff040 commit b3b1692

File tree

6 files changed

+112
-8
lines changed

6 files changed

+112
-8
lines changed

crates/templates/src/manager.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ impl TemplateManager {
125125

126126
for template_dir in template_dirs {
127127
let install_result = self
128-
.install_one(&template_dir, options, reporter)
128+
.install_one(&template_dir, options, source, reporter)
129129
.await
130130
.with_context(|| {
131131
format!("Failed to install template from {}", template_dir.display())
@@ -146,6 +146,7 @@ impl TemplateManager {
146146
&self,
147147
source_dir: &Path,
148148
options: &InstallOptions,
149+
source: &TemplateSource,
149150
reporter: &impl ProgressReporter,
150151
) -> anyhow::Result<InstallationResult> {
151152
let layout = TemplateLayout::new(source_dir);
@@ -179,11 +180,11 @@ impl TemplateManager {
179180
))
180181
}
181182
ExistsBehaviour::Update => {
182-
copy_template_over_existing(id, source_dir, &dest_dir).await?
183+
copy_template_over_existing(id, source_dir, &dest_dir, source).await?
183184
}
184185
}
185186
} else {
186-
copy_template_into(id, source_dir, &dest_dir).await?
187+
copy_template_into(id, source_dir, &dest_dir, source).await?
187188
};
188189

189190
Ok(InstallationResult::Installed(template))
@@ -237,6 +238,7 @@ async fn copy_template_over_existing(
237238
id: &str,
238239
source_dir: &Path,
239240
dest_dir: &Path,
241+
source: &TemplateSource,
240242
) -> anyhow::Result<Template> {
241243
// The nearby directory to which we initially copy the source
242244
let stage_dir = dest_dir.with_extension(".stage");
@@ -270,7 +272,9 @@ async fn copy_template_over_existing(
270272

271273
// Copy template source into stage directory, and do best effort
272274
// cleanup if it goes wrong.
273-
let copy_to_stage_err = copy_template_into(id, source_dir, &stage_dir).await.err();
275+
let copy_to_stage_err = copy_template_into(id, source_dir, &stage_dir, source)
276+
.await
277+
.err();
274278
if let Some(e) = copy_to_stage_err {
275279
let _ = tokio::fs::remove_dir_all(&stage_dir).await;
276280
return Err(e);
@@ -320,6 +324,7 @@ async fn copy_template_into(
320324
id: &str,
321325
source_dir: &Path,
322326
dest_dir: &Path,
327+
source: &TemplateSource,
323328
) -> anyhow::Result<Template> {
324329
tokio::fs::create_dir_all(&dest_dir)
325330
.await
@@ -340,9 +345,22 @@ async fn copy_template_into(
340345
)
341346
})?;
342347

348+
write_install_record(dest_dir, source);
349+
343350
load_template_from(id, dest_dir)
344351
}
345352

353+
fn write_install_record(dest_dir: &Path, source: &TemplateSource) {
354+
let layout = TemplateLayout::new(dest_dir);
355+
let install_record_path = layout.install_record_file();
356+
357+
// A failure here shouldn't fail the install
358+
let install_record = source.to_install_record();
359+
if let Ok(record_text) = toml::to_string_pretty(&install_record) {
360+
_ = std::fs::write(install_record_path, record_text);
361+
}
362+
}
363+
346364
fn load_template_from(id: &str, dest_dir: &Path) -> anyhow::Result<Template> {
347365
let layout = TemplateLayout::new(dest_dir);
348366
Template::load_from(&layout).with_context(|| {

crates/templates/src/reader.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::collections::HashMap;
22

33
use anyhow::Context;
44
use indexmap::IndexMap;
5-
use serde::Deserialize;
5+
use serde::{Deserialize, Serialize};
66

77
#[derive(Debug, Deserialize)]
88
#[serde(tag = "manifest_version")]
@@ -54,3 +54,15 @@ pub(crate) struct RawCustomFilter {
5454
pub(crate) fn parse_manifest_toml(text: impl AsRef<str>) -> anyhow::Result<RawTemplateManifest> {
5555
toml::from_str(text.as_ref()).context("Failed to parse template manifest TOML")
5656
}
57+
58+
#[derive(Debug, Deserialize, Serialize)]
59+
#[serde(rename_all = "snake_case", untagged)]
60+
pub(crate) enum RawInstalledFrom {
61+
Git { git: String },
62+
File { dir: String },
63+
}
64+
65+
pub(crate) fn parse_installed_from(text: impl AsRef<str>) -> Option<RawInstalledFrom> {
66+
// If we can't load it then it's not worth an error
67+
toml::from_str(text.as_ref()).ok()
68+
}

crates/templates/src/source.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,24 @@ impl TemplateSource {
5656
spin_version: spin_version.to_owned(),
5757
}))
5858
}
59+
60+
pub(crate) fn to_install_record(&self) -> Option<crate::reader::RawInstalledFrom> {
61+
match self {
62+
Self::Git(g) => Some(crate::reader::RawInstalledFrom::Git {
63+
git: g.url.to_string(),
64+
}),
65+
Self::File(p) => {
66+
// Saving a relative path would be meaningless (but should never happen)
67+
if p.is_absolute() {
68+
Some(crate::reader::RawInstalledFrom::File {
69+
dir: format!("{}", p.display()),
70+
})
71+
} else {
72+
None
73+
}
74+
}
75+
}
76+
}
5977
}
6078

6179
pub(crate) struct LocalTemplateSource {

crates/templates/src/store.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ const SNIPPETS_DIR_NAME: &str = "snippets";
7777

7878
const MANIFEST_FILE_NAME: &str = "spin-template.toml";
7979

80+
const INSTALLATION_RECORD_FILE_NAME: &str = ".install.toml";
81+
8082
impl TemplateLayout {
8183
pub fn new(template_dir: impl AsRef<Path>) -> Self {
8284
Self {
@@ -107,4 +109,8 @@ impl TemplateLayout {
107109
pub fn snippets_dir(&self) -> PathBuf {
108110
self.metadata_dir().join(SNIPPETS_DIR_NAME)
109111
}
112+
113+
pub fn install_record_file(&self) -> PathBuf {
114+
self.template_dir.join(INSTALLATION_RECORD_FILE_NAME)
115+
}
110116
}

crates/templates/src/template.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use crate::{
1717
pub struct Template {
1818
id: String,
1919
description: Option<String>,
20+
installed_from: InstalledFrom,
2021
trigger: TemplateTriggerCompatibility,
2122
variants: HashMap<TemplateVariantKind, TemplateVariant>,
2223
parameters: Vec<TemplateParameter>,
@@ -25,6 +26,13 @@ pub struct Template {
2526
content_dir: Option<PathBuf>, // TODO: maybe always need a spin.toml file in there?
2627
}
2728

29+
#[derive(Debug)]
30+
enum InstalledFrom {
31+
Git(String),
32+
Directory(String),
33+
Unknown,
34+
}
35+
2836
#[derive(Debug, Eq, PartialEq, Hash)]
2937
enum TemplateVariantKind {
3038
NewApplication,
@@ -115,10 +123,13 @@ impl Template {
115123
None
116124
};
117125

126+
let installed_from = read_install_record(layout);
127+
118128
let template = match raw {
119129
RawTemplateManifest::V1(raw) => Self {
120130
id: raw.id.clone(),
121131
description: raw.description.clone(),
132+
installed_from,
122133
trigger: Self::parse_trigger_type(raw.trigger_type, layout),
123134
variants: Self::parse_template_variants(raw.new_application, raw.add_component),
124135
parameters: Self::parse_parameters(&raw.parameters)?,
@@ -152,6 +163,16 @@ impl Template {
152163
}
153164
}
154165

166+
/// A human-readable description of where the template was installed
167+
/// from.
168+
pub fn installed_from_or_empty(&self) -> &str {
169+
match &self.installed_from {
170+
InstalledFrom::Git(repo) => repo,
171+
InstalledFrom::Directory(path) => path,
172+
InstalledFrom::Unknown => "",
173+
}
174+
}
175+
155176
fn variant(&self, variant_info: &TemplateVariantInfo) -> Option<&TemplateVariant> {
156177
let kind = variant_info.kind();
157178
self.variants.get(&kind)
@@ -390,3 +411,14 @@ fn parse_string_constraints(raw: &RawParameter) -> anyhow::Result<StringConstrai
390411

391412
Ok(StringConstraints { regex })
392413
}
414+
415+
fn read_install_record(layout: &TemplateLayout) -> InstalledFrom {
416+
use crate::reader::{parse_installed_from, RawInstalledFrom};
417+
418+
let installed_from_text = std::fs::read_to_string(layout.install_record_file()).ok();
419+
match installed_from_text.and_then(parse_installed_from) {
420+
Some(RawInstalledFrom::Git { git }) => InstalledFrom::Git(git),
421+
Some(RawInstalledFrom::File { dir }) => InstalledFrom::Directory(dir),
422+
None => InstalledFrom::Unknown,
423+
}
424+
}

src/commands/templates.rs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::path::PathBuf;
33
use anyhow::{Context, Result};
44
use clap::{Parser, Subcommand, ValueEnum};
55
use comfy_table::Table;
6+
use path_absolutize::Absolutize;
67

78
use serde::Serialize;
89
use spin_templates::{
@@ -87,7 +88,10 @@ impl Install {
8788
(Some(git), None) => {
8889
TemplateSource::try_from_git(git, &self.branch, env!("VERGEN_BUILD_SEMVER"))?
8990
}
90-
(None, Some(dir)) => TemplateSource::File(dir.clone()),
91+
(None, Some(dir)) => {
92+
let abs_dir = dir.absolutize().map(|d| d.to_path_buf());
93+
TemplateSource::File(abs_dir.unwrap_or_else(|_| dir.clone()))
94+
}
9195
_ => anyhow::bail!("Exactly one of `git` and `dir` sources must be specified"),
9296
};
9397

@@ -163,6 +167,10 @@ pub struct List {
163167
/// The format in which to list the templates.
164168
#[clap(value_enum, long = "format", default_value = "table", hide = true)]
165169
pub format: ListFormat,
170+
171+
/// Whether to show additional template details in the list.
172+
#[clap(long = "verbose", takes_value = false)]
173+
pub verbose: bool,
166174
}
167175

168176
#[derive(ValueEnum, Clone, Debug)]
@@ -198,11 +206,21 @@ impl List {
198206
println!();
199207
} else {
200208
let mut table = Table::new();
201-
table.set_header(vec!["Name", "Description"]);
209+
210+
let mut header = vec!["Name", "Description"];
211+
if self.verbose {
212+
header.push("Installed from");
213+
}
214+
215+
table.set_header(header);
202216
table.load_preset(comfy_table::presets::ASCII_BORDERS_ONLY_CONDENSED);
203217

204218
for template in templates {
205-
table.add_row(vec![template.id(), template.description_or_empty()]);
219+
let mut row = vec![template.id(), template.description_or_empty()];
220+
if self.verbose {
221+
row.push(template.installed_from_or_empty());
222+
}
223+
table.add_row(row);
206224
}
207225

208226
println!("{}", table);

0 commit comments

Comments
 (0)