Skip to content
Open
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
23 changes: 21 additions & 2 deletions packages/opensrc/cli/src/commands/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,15 @@ fn handle_repo(spec: &str, _cwd: &str, verbose: bool) -> Result<(), Box<dyn std:
};

let display = format!("{}/{}/{}", repo_spec.host, repo_spec.owner, repo_spec.repo);
let display_with_subpath = repo_spec
.subpath
.as_ref()
.map(|s| format!("{display}/{s}"))
.unwrap_or_else(|| display.clone());

// Check cache
if let Some(ref r) = repo_spec.git_ref {
if let Some(existing) = get_repo_info(&display) {
if let Some(existing) = get_repo_info(&display_with_subpath) {
if existing.version == *r {
let abs = get_absolute_path(&existing.path);
println!("{}", abs.display());
Expand All @@ -120,9 +125,23 @@ fn handle_repo(spec: &str, _cwd: &str, verbose: bool) -> Result<(), Box<dyn std:

log(
verbose,
&format!("Fetching {}/{}...", repo_spec.owner, repo_spec.repo),
&match &repo_spec.subpath {
Some(sp) => format!("Fetching {}/{}/{}...", repo_spec.owner, repo_spec.repo, sp),
None => format!("Fetching {}/{}...", repo_spec.owner, repo_spec.repo),
},
);
let resolved = resolve_repo(&repo_spec)?;

if repo_spec.git_ref.is_none() && repo_spec.subpath.is_some() {
if let Some(existing) = get_repo_info(&display_with_subpath) {
if existing.version == resolved.git_ref {
let abs = get_absolute_path(&existing.path);
println!("{}", abs.display());
return Ok(());
}
}
}

log(verbose, &format!(" → Cloning at {}...", resolved.git_ref));

let result = fetch_repo_source(&resolved);
Expand Down
29 changes: 23 additions & 6 deletions packages/opensrc/cli/src/commands/remove.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::core::cache::{
get_package_info, list_sources, remove_package_source, remove_repo_source, write_sources,
get_package_info, get_repo_info, list_sources, remove_package_source, remove_repo_source,
write_sources,
};
use crate::core::registries::repo::{is_repo_spec, parse_repo_spec};
use crate::core::registries::{detect_registry, Registry};
Expand All @@ -16,20 +17,36 @@ pub fn run(items: &[String]) -> Result<(), Box<dyn std::error::Error>> {
let is_repo = is_repo_spec(item) || (item.contains('/') && !item.contains(':'));

if is_repo {
let display_name = match parse_repo_spec(item) {
Some(spec) => format!("{}/{}/{}", spec.host, spec.owner, spec.repo),
let parsed = match parse_repo_spec(item) {
Some(spec) => spec,
None => {
println!(" ✗ Could not parse repo spec: {item}");
had_errors = true;
continue;
}
};
let display_name = format!("{}/{}/{}", parsed.host, parsed.owner, parsed.repo);
let sources_key = parsed
.subpath
.as_ref()
.map(|s| format!("{display_name}/{s}"))
.unwrap_or_else(|| display_name.clone());

let removal = if let Some(entry) = get_repo_info(&sources_key) {
remove_repo_source(&display_name, Some(&entry.version))
} else {
remove_repo_source(&display_name, None)
};

match remove_repo_source(&display_name, None) {
match removal {
Ok(true) => {
println!(" ✓ Removed {display_name}");
if let Some(sp) = &parsed.subpath {
println!(" ✓ Removed {display_name}/{sp}");
} else {
println!(" ✓ Removed {display_name}");
}
removed += 1;
removed_repos.push(display_name);
removed_repos.push(sources_key);
}
Ok(false) => {
println!(" ⚠ {item} not found");
Expand Down
104 changes: 99 additions & 5 deletions packages/opensrc/cli/src/core/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,80 @@ fn clone_at_tag(repo_url: &str, target: &Path, version: &str) -> CloneResult {
}
}

fn clone_at_ref_sparse(repo_url: &str, target: &Path, git_ref: &str, subpath: &str) -> CloneResult {
let target_str = target.to_string_lossy();

let output = git_clone_output(&[
"clone",
"--filter=blob:none",
"--sparse",
"--depth",
"1",
"--branch",
git_ref,
"--single-branch",
repo_url,
&target_str,
]);

match output {
Ok(o) if o.status.success() => {}
Ok(o) => {
let _ = fs::remove_dir_all(target);
let stderr = stderr_string(&o);
let msg = if stderr.is_empty() {
"Failed to sparse clone repository".to_string()
} else {
format!("Failed to sparse clone repository: {stderr}")
};
return CloneResult {
success: false,
error: Some(msg),
};
}
Err(e) => {
return CloneResult {
success: false,
error: Some(format!("Failed to run git: {e}")),
};
}
}

let sparse_out = Command::new("git")
.current_dir(target)
.args(["sparse-checkout", "set", subpath])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.output();

match sparse_out {
Ok(o) if o.status.success() => CloneResult {
success: true,
error: None,
},
Ok(o) => {
let _ = fs::remove_dir_all(target);
let stderr = stderr_string(&o);
let msg = if stderr.is_empty() {
"Failed to run git sparse-checkout".to_string()
} else {
format!("Failed to run git sparse-checkout: {stderr}")
};
CloneResult {
success: false,
error: Some(msg),
}
}
Err(e) => {
let _ = fs::remove_dir_all(target);
CloneResult {
success: false,
error: Some(format!("Failed to run git: {e}")),
}
}
}
}

fn clone_at_ref(repo_url: &str, target: &Path, git_ref: &str) -> CloneResult {
let target_str = target.to_string_lossy();

Expand Down Expand Up @@ -224,7 +298,7 @@ pub fn fetch_source(resolved: &ResolvedPackage) -> FetchResult {
pub fn fetch_repo_source(resolved: &ResolvedRepo) -> FetchResult {
let repo_path = get_repo_path(&resolved.display_name, &resolved.git_ref);

if repo_path.exists() {
if resolved.subpath.is_none() && repo_path.exists() {
return FetchResult {
package: resolved.display_name.clone(),
version: resolved.git_ref.clone(),
Expand All @@ -235,16 +309,26 @@ pub fn fetch_repo_source(resolved: &ResolvedRepo) -> FetchResult {
};
}

if resolved.subpath.is_some() && repo_path.exists() {
let _ = fs::remove_dir_all(&repo_path);
Comment on lines +312 to +313
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if resolved.subpath.is_some() && repo_path.exists() {
let _ = fs::remove_dir_all(&repo_path);
if let Some(ref sp) = resolved.subpath {
if repo_path.exists() {
// If the requested subpath already exists on disk, reuse it instead
// of deleting the entire repo directory (which would destroy files
// checked out for other subpaths of the same repo/ref).
if repo_path.join(sp).exists() {
return FetchResult {
package: format!("{}/{}", resolved.display_name, sp),
version: resolved.git_ref.clone(),
path: format!(
"{}/{}",
get_repo_relative_path(&resolved.display_name, &resolved.git_ref),
sp
),
success: true,
error: None,
registry: None,
};
}
// Subpath not present in the existing clone — must re-clone.
// This will remove files from any previously sparse-checked-out
// subpath; callers should handle stale sources.json entries.
let _ = fs::remove_dir_all(&repo_path);
}

Fetching a different subpath of the same repo at the same ref deletes and re-clones the shared directory, destroying previously fetched subpath files while their sources.json entries remain as dangling references.

Fix on Vercel

}

if let Some(parent) = repo_path.parent() {
let _ = fs::create_dir_all(parent);
}

let clone_url = authenticated_clone_url(&resolved.repo_url);
let clone = clone_at_ref(&clone_url, &repo_path, &resolved.git_ref);
let clone = match resolved.subpath.as_deref() {
Some(sp) => clone_at_ref_sparse(&clone_url, &repo_path, &resolved.git_ref, sp),
None => clone_at_ref(&clone_url, &repo_path, &resolved.git_ref),
};

if !clone.success {
return FetchResult {
package: resolved.display_name.clone(),
package: match &resolved.subpath {
Some(sp) => format!("{}/{}", resolved.display_name, sp),
None => resolved.display_name.clone(),
},
version: resolved.git_ref.clone(),
path: get_repo_relative_path(&resolved.display_name, &resolved.git_ref),
success: false,
Expand All @@ -256,9 +340,19 @@ pub fn fetch_repo_source(resolved: &ResolvedRepo) -> FetchResult {
remove_git_dir(&repo_path);

FetchResult {
package: resolved.display_name.clone(),
package: match &resolved.subpath {
Some(sp) => format!("{}/{}", resolved.display_name, sp),
None => resolved.display_name.clone(),
},
version: resolved.git_ref.clone(),
path: get_repo_relative_path(&resolved.display_name, &resolved.git_ref),
path: match &resolved.subpath {
Some(sp) => format!(
"{}/{}",
get_repo_relative_path(&resolved.display_name, &resolved.git_ref),
sp
),
None => get_repo_relative_path(&resolved.display_name, &resolved.git_ref),
},
success: true,
error: clone.error,
registry: None,
Expand Down
5 changes: 5 additions & 0 deletions packages/opensrc/cli/src/core/registries/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,4 +245,9 @@ mod tests {
"https://github.com/owner/repo"
);
}

#[test]
fn test_detect_input_type_repo_subpath() {
assert_eq!(detect_input_type("vercel/ai/packages/ai"), "repo");
}
}
Loading