use crate::fs_paths::Paths;
use crate::git::{parse_repo_ref, GitCli};
use crate::lock::{Lockfile, LockedMarketplace};
use crate::profile::Profile;
use crate::vendor::{self, PluginSource};
use crate::vendor_fs;
use std::io::Write;
use std::path::{Path, PathBuf};
pub fn ensure_marketplace_clones<G: GitCli>(git: &G, profile: &Profile, paths: &Paths) -> anyhow::Result<()> {
for (name, source) in &profile.marketplaces {
let dir = paths.marketplace_clone_dir(name);
if dir.is_dir() {
continue;
}
let repo_ref = parse_repo_ref(source)?;
git.clone(&repo_ref.clone_url(), &dir)?;
}
Ok(())
}
pub fn vendor_plugins<G: GitCli>(
git: &G,
profile: &Profile,
profile_key: &str,
cwd: &Path,
paths: &Paths,
force: bool,
lock: &mut Lockfile,
) -> anyhow::Result<()> {
for plugin_id in &profile.plugins {
let dest = paths.profile_vendor_dir(profile_key).join(plugin_id);
if dest.exists() {
if !force {
continue;
}
std::fs::remove_dir_all(&dest)?;
}
let (name, marketplace) = plugin_id
.rsplit_once('@')
.ok_or_else(|| anyhow::anyhow!("plugin id '{plugin_id}' is missing '@marketplace'"))?;
if marketplace == "skills-dir" {
vendor_loose_skill(name, cwd, paths, &dest)?;
continue;
}
vendor_marketplace_entry(git, paths, marketplace, name, plugin_id, force, &dest, lock)?;
}
Ok(())
}
fn vendor_loose_skill(name: &str, cwd: &Path, paths: &Paths, dest: &Path) -> anyhow::Result<()> {
let src = locate_loose_skill(name, cwd, paths)?;
vendor_fs::copy_dir_atomic(&src, dest)?;
vendor_fs::ensure_manifest(dest, name)
}
fn vendor_marketplace_entry<G: GitCli>(
git: &G,
paths: &Paths,
marketplace: &str,
name: &str,
plugin_id: &str,
force: bool,
dest: &Path,
lock: &mut Lockfile,
) -> anyhow::Result<()> {
let mkt_dir = paths.marketplace_clone_dir(marketplace);
let manifest_body = std::fs::read_to_string(mkt_dir.join(".claude-plugin").join("marketplace.json"))
.map_err(|e| anyhow::anyhow!("reading marketplace.json for '{marketplace}': {e}"))?;
let plugins = vendor::parse_marketplace_json(&manifest_body)?;
let entry = vendor::find_plugin(&plugins, name)?;
match &entry.source {
PluginSource::RelativePath(rel) => {
let src = mkt_dir.join(rel.trim_start_matches("./"));
vendor_fs::copy_dir_atomic(&src, dest)
}
PluginSource::ExternalRepo { repo } => vendor_external_repo(git, paths, repo, plugin_id, force, dest, lock),
PluginSource::GitSubdir { url, path, git_ref, sha } => vendor_git_subdir(
git, paths, url, path, git_ref.as_deref(), sha.as_deref(), plugin_id, force, dest, lock,
),
}
}
fn vendor_external_repo<G: GitCli>(
git: &G,
paths: &Paths,
repo: &str,
plugin_id: &str,
force: bool,
dest: &Path,
lock: &mut Lockfile,
) -> anyhow::Result<()> {
let repo_ref = parse_repo_ref(repo)?;
let ext_dir = paths.external_marketplace_dir(&repo_ref.owner, &repo_ref.repo);
if !ext_dir.is_dir() {
git.clone(&repo_ref.clone_url(), &ext_dir)?;
} else if force {
git.pull(&ext_dir)?;
}
let sha = git.head_sha(&ext_dir)?;
lock.plugins.insert(plugin_id.to_string(), LockedMarketplace { source: repo.to_string(), sha });
vendor_fs::copy_dir_atomic(&ext_dir, dest)
}
#[allow(clippy::too_many_arguments)]
fn vendor_git_subdir<G: GitCli>(
git: &G,
paths: &Paths,
url: &str,
subpath: &str,
git_ref: Option<&str>,
pinned_sha: Option<&str>,
plugin_id: &str,
force: bool,
dest: &Path,
lock: &mut Lockfile,
) -> anyhow::Result<()> {
let repo_ref = parse_repo_ref(url)?;
let ext_dir = paths.external_marketplace_dir(&repo_ref.owner, &repo_ref.repo);
if !ext_dir.is_dir() {
git.clone(&repo_ref.clone_url(), &ext_dir)?;
} else if force {
git.pull(&ext_dir)?;
}
if let Some(target) = pinned_sha.or(git_ref) {
git.checkout(&ext_dir, target)?;
}
let sha = git.head_sha(&ext_dir)?;
lock.plugins.insert(plugin_id.to_string(), LockedMarketplace { source: url.to_string(), sha });
vendor_fs::copy_dir_atomic(&ext_dir.join(subpath), dest)
}
fn locate_loose_skill(name: &str, cwd: &Path, paths: &Paths) -> anyhow::Result<PathBuf> {
let project = cwd.join(".claude").join("skills").join(name);
if project.is_dir() {
return Ok(project);
}
let personal = paths.claude_skills_dir().join(name);
if personal.is_dir() {
return Ok(personal);
}
anyhow::bail!("loose skill '{name}' not found under .claude/skills or ~/.claude/skills")
}
fn confirm(profile: &Profile, missing_marketplaces: &[String], missing_plugins: &[String]) -> bool {
println!("claude-profile will vendor the following into ~/.claude-profiles/store/{}/:", profile.name);
for name in missing_marketplaces {
println!(" marketplace clone: {name}");
}
for id in missing_plugins {
println!(" plugin/skill: {id}");
}
print!("Proceed? [y/N] ");
std::io::stdout().flush().ok();
let mut line = String::new();
if std::io::stdin().read_line(&mut line).is_err() {
return false;
}
matches!(line.trim(), "y" | "Y" | "yes")
}
pub fn provision<G: GitCli>(
git: &G,
profile: &Profile,
profile_key: &str,
_cwd: &Path,
paths: &Paths,
assume_yes: bool,
) -> anyhow::Result<()> {
let missing_marketplaces: Vec<String> = profile
.marketplaces
.keys()
.filter(|name| !paths.marketplace_clone_dir(name).is_dir())
.cloned()
.collect();
let missing_plugins: Vec<String> = profile
.plugins
.iter()
.filter(|id| !paths.profile_vendor_dir(profile_key).join(id).exists())
.cloned()
.collect();
if !missing_marketplaces.is_empty() || !missing_plugins.is_empty() {
if !assume_yes && !confirm(profile, &missing_marketplaces, &missing_plugins) {
anyhow::bail!("provisioning declined by user");
}
}
ensure_marketplace_clones(git, profile, paths)
}
fn checkout_target(
update_floating: bool,
locked: Option<&LockedMarketplace>,
explicit_ref: &Option<String>,
) -> Option<String> {
if update_floating {
return explicit_ref.clone();
}
match locked {
Some(l) if !l.sha.is_empty() => Some(l.sha.clone()),
_ => explicit_ref.clone(),
}
}
pub fn pin_marketplaces<G: GitCli>(
git: &G,
profile: &Profile,
mkt_install_dir: &dyn Fn(&str) -> PathBuf,
lock: &mut Lockfile,
update_floating: bool,
) -> anyhow::Result<()> {
for (name, source) in &profile.marketplaces {
let resolved = resolve_marketplace_sha(git, name, source, mkt_install_dir, lock, update_floating)?;
lock.marketplaces
.insert(name.clone(), LockedMarketplace { source: source.clone(), sha: resolved });
}
Ok(())
}
fn resolve_marketplace_sha<G: GitCli>(
git: &G,
name: &str,
source: &str,
mkt_install_dir: &dyn Fn(&str) -> PathBuf,
lock: &Lockfile,
update_floating: bool,
) -> anyhow::Result<String> {
let repo_ref = parse_repo_ref(source)?;
let dir = mkt_install_dir(name);
if !git.is_repo(&dir) {
eprintln!(
"WARNING: marketplace '{name}' at {} is not a git checkout; recording unpinned (not reproducible)",
dir.display()
);
return Ok(String::new());
}
let target = checkout_target(update_floating, lock.marketplaces.get(name), &repo_ref.git_ref);
if let Some(ref t) = target {
git.checkout(&dir, t)?;
} else if update_floating {
git.advance_to_remote_head(&dir)?;
}
git.head_sha(&dir)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::git::GitCli;
use std::cell::RefCell;
use std::fs;
use std::path::{Path, PathBuf};
struct MockGit {
cloned: RefCell<Vec<(String, PathBuf)>>,
pulled: RefCell<Vec<PathBuf>>,
advanced: RefCell<Vec<PathBuf>>,
checkouts: RefCell<Vec<(PathBuf, String)>>,
head: String,
present: bool,
}
impl MockGit {
fn new(head: &str) -> Self {
MockGit {
cloned: RefCell::new(vec![]), pulled: RefCell::new(vec![]),
advanced: RefCell::new(vec![]),
checkouts: RefCell::new(vec![]), head: head.into(), present: true,
}
}
}
impl GitCli for MockGit {
fn advance_to_remote_head(&self, r: &Path) -> anyhow::Result<()> {
self.advanced.borrow_mut().push(r.to_path_buf());
Ok(())
}
fn clone(&self, url: &str, dest: &Path) -> anyhow::Result<()> {
self.cloned.borrow_mut().push((url.to_string(), dest.to_path_buf()));
fs::create_dir_all(dest)?;
Ok(())
}
fn pull(&self, r: &Path) -> anyhow::Result<()> {
self.pulled.borrow_mut().push(r.to_path_buf());
Ok(())
}
fn head_sha(&self, _r: &Path) -> anyhow::Result<String> { Ok(self.head.clone()) }
fn checkout(&self, r: &Path, gr: &str) -> anyhow::Result<()> {
self.checkouts.borrow_mut().push((r.to_path_buf(), gr.to_string()));
Ok(())
}
fn is_repo(&self, _r: &Path) -> bool { self.present }
fn sparse_fetch(&self, _u: &str, _d: &Path, _s: &str) -> anyhow::Result<()> { Ok(()) }
}
fn profile(json: &str) -> crate::profile::Profile {
crate::profile::Profile::from_json_str(json).unwrap()
}
#[test]
fn ensure_marketplace_clones_clones_missing_marketplaces_only() {
let tmp = tempfile::tempdir().unwrap();
let paths = crate::fs_paths::Paths::from_home(tmp.path().to_path_buf());
let p = profile(r#"{"name":"p","marketplaces":{"m":"o/r"}}"#);
let git = MockGit::new("sha1");
ensure_marketplace_clones(&git, &p, &paths).unwrap();
assert_eq!(git.cloned.borrow().len(), 1);
assert!(paths.marketplace_clone_dir("m").is_dir());
ensure_marketplace_clones(&git, &p, &paths).unwrap();
assert_eq!(git.cloned.borrow().len(), 1);
}
#[test]
fn vendors_relative_path_plugin_from_marketplace_clone() {
let tmp = tempfile::tempdir().unwrap();
let paths = crate::fs_paths::Paths::from_home(tmp.path().to_path_buf());
let mkt_dir = paths.marketplace_clone_dir("m");
fs::create_dir_all(mkt_dir.join(".claude-plugin")).unwrap();
fs::write(
mkt_dir.join(".claude-plugin").join("marketplace.json"),
r#"{"plugins":[{"name":"foo","source":"./skills/foo"}]}"#,
).unwrap();
fs::create_dir_all(mkt_dir.join("skills").join("foo")).unwrap();
fs::write(mkt_dir.join("skills").join("foo").join("SKILL.md"), "# foo").unwrap();
let p = profile(r#"{"name":"p","marketplaces":{"m":"o/r"},"plugins":["foo@m"]}"#);
let git = MockGit::new("sha1");
let mut lock = crate::lock::Lockfile::new("p");
vendor_plugins(&git, &p, "p", tmp.path(), &paths, false, &mut lock).unwrap();
let vendored = paths.profile_vendor_dir("p").join("foo@m");
assert!(vendored.join("SKILL.md").is_file());
}
#[test]
fn vendors_external_repo_plugin_by_cloning_it_and_records_sha() {
let tmp = tempfile::tempdir().unwrap();
let paths = crate::fs_paths::Paths::from_home(tmp.path().to_path_buf());
let mkt_dir = paths.marketplace_clone_dir("m");
fs::create_dir_all(mkt_dir.join(".claude-plugin")).unwrap();
fs::write(
mkt_dir.join(".claude-plugin").join("marketplace.json"),
r#"{"plugins":[{"name":"ext","source":{"source":"github","repo":"owner/ext"}}]}"#,
).unwrap();
let p = profile(r#"{"name":"p","marketplaces":{"m":"o/r"},"plugins":["ext@m"]}"#);
let git = MockGit::new("sha1");
let mut lock = crate::lock::Lockfile::new("p");
vendor_plugins(&git, &p, "p", tmp.path(), &paths, false, &mut lock).unwrap();
assert!(git.cloned.borrow().iter().any(|(url, _)| url.contains("owner/ext")));
assert!(paths.profile_vendor_dir("p").join("ext@m").is_dir());
let locked = lock.plugins.get("ext@m").expect("external plugin recorded in lock");
assert_eq!(locked.source, "owner/ext");
assert_eq!(locked.sha, "sha1");
}
#[test]
fn vendors_git_subdir_plugin_copies_only_subpath_and_checks_out_pinned_sha() {
let tmp = tempfile::tempdir().unwrap();
let paths = crate::fs_paths::Paths::from_home(tmp.path().to_path_buf());
let mkt_dir = paths.marketplace_clone_dir("m");
fs::create_dir_all(mkt_dir.join(".claude-plugin")).unwrap();
fs::write(
mkt_dir.join(".claude-plugin").join("marketplace.json"),
r#"{"plugins":[{"name":"sub","source":{
"source":"git-subdir",
"url":"https://github.com/owner/repo.git",
"path":"plugins/sub",
"sha":"pinned123"
}}]}"#,
).unwrap();
let ext_dir = paths.external_marketplace_dir("owner", "repo");
fs::create_dir_all(ext_dir.join("plugins").join("sub")).unwrap();
fs::write(ext_dir.join("plugins").join("sub").join("SKILL.md"), "# sub").unwrap();
fs::create_dir_all(ext_dir.join("plugins").join("other")).unwrap();
let p = profile(r#"{"name":"p","marketplaces":{"m":"o/r"},"plugins":["sub@m"]}"#);
let git = MockGit::new("head_after_checkout");
let mut lock = crate::lock::Lockfile::new("p");
vendor_plugins(&git, &p, "p", tmp.path(), &paths, false, &mut lock).unwrap();
assert_eq!(git.checkouts.borrow()[0], (ext_dir.clone(), "pinned123".to_string()));
let vendored = paths.profile_vendor_dir("p").join("sub@m");
assert!(vendored.join("SKILL.md").is_file());
assert!(!vendored.join("other").exists());
let locked = lock.plugins.get("sub@m").expect("recorded in lock");
assert_eq!(locked.sha, "head_after_checkout");
assert_eq!(locked.source, "https://github.com/owner/repo.git");
}
#[test]
fn vendors_bare_url_plugin_same_as_external_repo() {
let tmp = tempfile::tempdir().unwrap();
let paths = crate::fs_paths::Paths::from_home(tmp.path().to_path_buf());
let mkt_dir = paths.marketplace_clone_dir("m");
fs::create_dir_all(mkt_dir.join(".claude-plugin")).unwrap();
fs::write(
mkt_dir.join(".claude-plugin").join("marketplace.json"),
r#"{"plugins":[{"name":"superpowers","source":{"source":"url","url":"https://github.com/obra/superpowers.git"}}]}"#,
).unwrap();
let p = profile(r#"{"name":"p","marketplaces":{"m":"o/r"},"plugins":["superpowers@m"]}"#);
let git = MockGit::new("sha1");
let mut lock = crate::lock::Lockfile::new("p");
vendor_plugins(&git, &p, "p", tmp.path(), &paths, false, &mut lock).unwrap();
assert!(git.cloned.borrow().iter().any(|(url, _)| url == "https://github.com/obra/superpowers.git"));
assert!(paths.profile_vendor_dir("p").join("superpowers@m").is_dir());
let locked = lock.plugins.get("superpowers@m").expect("recorded in lock");
assert_eq!(locked.source, "https://github.com/obra/superpowers.git");
assert_eq!(locked.sha, "sha1");
}
#[test]
fn force_revendor_pulls_external_repo_and_updates_locked_sha() {
let tmp = tempfile::tempdir().unwrap();
let paths = crate::fs_paths::Paths::from_home(tmp.path().to_path_buf());
let mkt_dir = paths.marketplace_clone_dir("m");
fs::create_dir_all(mkt_dir.join(".claude-plugin")).unwrap();
fs::write(
mkt_dir.join(".claude-plugin").join("marketplace.json"),
r#"{"plugins":[{"name":"ext","source":{"source":"github","repo":"owner/ext"}}]}"#,
).unwrap();
let p = profile(r#"{"name":"p","marketplaces":{"m":"o/r"},"plugins":["ext@m"]}"#);
let git = MockGit::new("sha1");
let mut lock = crate::lock::Lockfile::new("p");
vendor_plugins(&git, &p, "p", tmp.path(), &paths, false, &mut lock).unwrap();
assert!(git.pulled.borrow().is_empty());
vendor_plugins(&git, &p, "p", tmp.path(), &paths, true, &mut lock).unwrap();
let ext_dir = paths.external_marketplace_dir("owner", "ext");
assert_eq!(git.pulled.borrow().as_slice(), &[ext_dir]); assert_eq!(lock.plugins.get("ext@m").unwrap().sha, "sha1"); }
#[test]
fn vendors_loose_skill_and_generates_manifest() {
let tmp = tempfile::tempdir().unwrap();
let paths = crate::fs_paths::Paths::from_home(tmp.path().to_path_buf());
let personal_skill = paths.claude_skills_dir().join("my-skill");
fs::create_dir_all(&personal_skill).unwrap();
fs::write(personal_skill.join("SKILL.md"), "# my-skill").unwrap();
let p = profile(r#"{"name":"p","plugins":["my-skill@skills-dir"]}"#);
let git = MockGit::new("sha1");
let mut lock = crate::lock::Lockfile::new("p");
vendor_plugins(&git, &p, "p", tmp.path(), &paths, false, &mut lock).unwrap();
let vendored = paths.profile_vendor_dir("p").join("my-skill@skills-dir");
assert!(vendored.join("SKILL.md").is_file());
assert!(vendored.join(".claude-plugin").join("plugin.json").is_file());
assert!(!personal_skill.join(".claude-plugin").exists());
}
#[test]
fn skips_already_vendored_plugin_unless_forced() {
let tmp = tempfile::tempdir().unwrap();
let paths = crate::fs_paths::Paths::from_home(tmp.path().to_path_buf());
let mkt_dir = paths.marketplace_clone_dir("m");
fs::create_dir_all(mkt_dir.join(".claude-plugin")).unwrap();
fs::write(
mkt_dir.join(".claude-plugin").join("marketplace.json"),
r#"{"plugins":[{"name":"foo","source":"./foo"}]}"#,
).unwrap();
fs::create_dir_all(mkt_dir.join("foo")).unwrap();
fs::write(mkt_dir.join("foo").join("a.txt"), "v1").unwrap();
let p = profile(r#"{"name":"p","marketplaces":{"m":"o/r"},"plugins":["foo@m"]}"#);
let git = MockGit::new("sha1");
let mut lock = crate::lock::Lockfile::new("p");
vendor_plugins(&git, &p, "p", tmp.path(), &paths, false, &mut lock).unwrap();
fs::write(mkt_dir.join("foo").join("a.txt"), "v2").unwrap();
vendor_plugins(&git, &p, "p", tmp.path(), &paths, false, &mut lock).unwrap();
let vendored = paths.profile_vendor_dir("p").join("foo@m");
assert_eq!(fs::read_to_string(vendored.join("a.txt")).unwrap(), "v1");
vendor_plugins(&git, &p, "p", tmp.path(), &paths, true, &mut lock).unwrap();
assert_eq!(fs::read_to_string(vendored.join("a.txt")).unwrap(), "v2");
}
#[test]
fn errors_on_unresolvable_loose_skill() {
let tmp = tempfile::tempdir().unwrap();
let paths = crate::fs_paths::Paths::from_home(tmp.path().to_path_buf());
let p = profile(r#"{"name":"p","plugins":["missing@skills-dir"]}"#);
let git = MockGit::new("sha1");
let mut lock = crate::lock::Lockfile::new("p");
assert!(vendor_plugins(&git, &p, "p", tmp.path(), &paths, false, &mut lock).is_err());
}
#[test]
fn pins_explicit_ref_and_records_sha() {
let git = MockGit::new("sha_after_v1");
let dir = |_n: &str| PathBuf::from("/mkts/m");
let mut lock = crate::lock::Lockfile::new("p");
let p = profile(r#"{"name":"p","marketplaces":{"m":"o/r#v1"}}"#);
pin_marketplaces(&git, &p, &dir, &mut lock, false).unwrap();
assert_eq!(git.checkouts.borrow()[0], (PathBuf::from("/mkts/m"), "v1".to_string()));
assert_eq!(lock.marketplaces.get("m").unwrap().sha, "sha_after_v1");
assert_eq!(lock.marketplaces.get("m").unwrap().source, "o/r#v1");
}
#[test]
fn uses_locked_sha_when_not_updating_floating() {
let profile = crate::profile::Profile::from_json_str(
r#"{"name":"p","marketplaces":{"m":"o/r"}}"#).unwrap(); let git = MockGit::new("locked_sha");
let dir = |_n: &str| PathBuf::from("/mkts/m");
let mut lock = crate::lock::Lockfile::new("p");
lock.marketplaces.insert("m".into(),
crate::lock::LockedMarketplace { source: "o/r".into(), sha: "locked_sha".into() });
pin_marketplaces(&git, &profile, &dir, &mut lock, false).unwrap();
assert_eq!(git.checkouts.borrow()[0].1, "locked_sha");
assert_eq!(lock.marketplaces.get("m").unwrap().sha, "locked_sha");
}
#[test]
fn update_floating_moves_to_current_head() {
let profile = crate::profile::Profile::from_json_str(
r#"{"name":"p","marketplaces":{"m":"o/r"}}"#).unwrap();
let git = MockGit::new("new_head");
let dir = |_n: &str| PathBuf::from("/mkts/m");
let mut lock = crate::lock::Lockfile::new("p");
lock.marketplaces.insert("m".into(),
crate::lock::LockedMarketplace { source: "o/r".into(), sha: "old".into() });
pin_marketplaces(&git, &profile, &dir, &mut lock, true).unwrap();
assert!(git.checkouts.borrow().is_empty());
assert_eq!(lock.marketplaces.get("m").unwrap().sha, "new_head");
assert_eq!(git.advanced.borrow().as_slice(), &[PathBuf::from("/mkts/m")]);
}
#[test]
fn locked_sha_wins_over_explicit_ref() {
let profile = crate::profile::Profile::from_json_str(
r#"{"name":"p","marketplaces":{"m":"o/r#v2"}}"#).unwrap(); let git = MockGit::new("head_sha");
let dir = |_n: &str| PathBuf::from("/mkts/m");
let mut lock = crate::lock::Lockfile::new("p");
lock.marketplaces.insert("m".into(),
crate::lock::LockedMarketplace { source: "o/r#v2".into(), sha: "locked".into() });
pin_marketplaces(&git, &profile, &dir, &mut lock, false).unwrap();
assert_eq!(git.checkouts.borrow()[0].1, "locked");
assert_eq!(lock.marketplaces.get("m").unwrap().sha, "head_sha");
}
#[test]
fn non_git_marketplace_degrades_gracefully() {
let profile = crate::profile::Profile::from_json_str(
r#"{"name":"p","marketplaces":{"m":"o/r"}}"#).unwrap();
let mut git = MockGit::new("SHOULD_NOT_BE_USED");
git.present = false; let dir = |_n: &str| PathBuf::from("/mkts/m");
let mut lock = crate::lock::Lockfile::new("p");
pin_marketplaces(&git, &profile, &dir, &mut lock, false).unwrap();
assert!(git.checkouts.borrow().is_empty());
let locked = lock.marketplaces.get("m").expect("marketplace still recorded in lock");
assert_eq!(locked.sha, "");
assert_eq!(locked.source, "o/r");
}
#[test]
fn update_floating_keeps_explicit_ref_pinned() {
let profile = crate::profile::Profile::from_json_str(
r#"{"name":"p","marketplaces":{"m":"o/r#v3"}}"#).unwrap(); let git = MockGit::new("new_head");
let dir = |_n: &str| PathBuf::from("/mkts/m");
let mut lock = crate::lock::Lockfile::new("p");
lock.marketplaces.insert("m".into(),
crate::lock::LockedMarketplace { source: "o/r#v3".into(), sha: "old_sha".into() });
pin_marketplaces(&git, &profile, &dir, &mut lock, true).unwrap();
assert_eq!(git.checkouts.borrow()[0].1, "v3");
assert!(git.advanced.borrow().is_empty());
}
#[test]
fn stale_empty_locked_sha_is_not_checked_out() {
let profile = crate::profile::Profile::from_json_str(
r#"{"name":"p","marketplaces":{"m":"o/r"}}"#).unwrap(); let git = MockGit::new("head_sha"); let dir = |_n: &str| PathBuf::from("/mkts/m");
let mut lock = crate::lock::Lockfile::new("p");
lock.marketplaces.insert("m".into(),
crate::lock::LockedMarketplace { source: "o/r".into(), sha: "".into() });
pin_marketplaces(&git, &profile, &dir, &mut lock, false).unwrap();
assert!(git.checkouts.borrow().is_empty());
assert_eq!(lock.marketplaces.get("m").unwrap().sha, "head_sha");
}
}