fix: provider API calls used the local folder name, not the actual repo name
Every place building an owner/repo slug for the provider API (refresh, release, add_collaborator) used path.file_name() as the repo segment - the local checkout's directory name. That's usually the same as the GitHub repo name, but not always: vulp's own folder is 'vulp', its GitHub repo is 'fxg', so every refresh silently 404'd fetching its own metadata/PRs/issues/CI (the cache carried an empty meta block with fetched_at: 0, never actually set). RepoSummary now also carries remote_repo_name, parsed from the git remote URL alongside owner (same source, same parsing approach) - the segment the provider actually knows the repo by. All four slug-building call sites now prefer it, falling back to the folder name only when there's no remote to parse it from. Verified: 'vulp refresh' now correctly hits dualfroz/fxg and the cache shows real stars/issues/PRs for vulp instead of the empty stub.
This commit is contained in:
1 parent
4b0a6e324f
commit
3eb922559c
3 files changed
+52
-13
No files matched your search
+12
-7
@@ -95,8 +95,9 @@ pub async fn release(cfg: &AppConfig, guard: super::Guard, repo: &str, tag: &str
|
|||||||
let accounts = crate::auth::load_accounts(cfg).unwrap_or_default();
|
let accounts = crate::auth::load_accounts(cfg).unwrap_or_default();
|
||||||
if let Some(acc) = accounts.iter().find(|a| a.is_default).or(accounts.first()) {
|
if let Some(acc) = accounts.iter().find(|a| a.is_default).or(accounts.first()) {
|
||||||
if let Ok(provider) = crate::providers::make_provider(acc) {
|
if let Ok(provider) = crate::providers::make_provider(acc) {
|
||||||
let repo_basename = r.path.file_name().map(|b| b.to_string_lossy().to_string())
|
let repo_basename = r.remote_repo_name.clone().unwrap_or_else(|| {
|
||||||
.unwrap_or_else(|| r.name.clone());
|
r.path.file_name().map(|b| b.to_string_lossy().to_string()).unwrap_or_else(|| r.name.clone())
|
||||||
|
});
|
||||||
match provider.create_release(&repo_basename, tag, tag, notes, false).await {
|
match provider.create_release(&repo_basename, tag, tag, notes, false).await {
|
||||||
Ok(url) => println!("{url}"),
|
Ok(url) => println!("{url}"),
|
||||||
Err(e) => eprintln!("warning: tag pushed, but provider release failed: {e:#}"),
|
Err(e) => eprintln!("warning: tag pushed, but provider release failed: {e:#}"),
|
||||||
@@ -168,8 +169,9 @@ pub async fn repo_cmd(cfg: &AppConfig, json: bool, guard: super::Guard, cmd: Rep
|
|||||||
}
|
}
|
||||||
let r = find_repo(cfg, &repo);
|
let r = find_repo(cfg, &repo);
|
||||||
super::guard_protected(cfg, guard.force, &[&r]);
|
super::guard_protected(cfg, guard.force, &[&r]);
|
||||||
let repo_basename = r.path.file_name().map(|b| b.to_string_lossy().to_string())
|
let repo_basename = r.remote_repo_name.clone().unwrap_or_else(|| {
|
||||||
.unwrap_or_else(|| r.name.clone());
|
r.path.file_name().map(|b| b.to_string_lossy().to_string()).unwrap_or_else(|| r.name.clone())
|
||||||
|
});
|
||||||
if guard.dry_run {
|
if guard.dry_run {
|
||||||
println!("DRY RUN: would invite {user} to {repo_basename} ({role})");
|
println!("DRY RUN: would invite {user} to {repo_basename} ({role})");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -316,11 +318,14 @@ pub async fn refresh(cfg: &AppConfig, json: bool) -> Result<()> {
|
|||||||
let repos = super::scan_repos(cfg);
|
let repos = super::scan_repos(cfg);
|
||||||
|
|
||||||
// Org-owned repos are reachable through the personal token — refresh
|
// Org-owned repos are reachable through the personal token — refresh
|
||||||
// everything with an owner, addressed by full "owner/basename" slug.
|
// everything with an owner, addressed by full "owner/repo" slug. The
|
||||||
|
// repo name comes from the remote URL, not the local folder name — they
|
||||||
|
// can differ (vulp's own folder is "vulp", its GitHub repo is "fxg").
|
||||||
let targets: Vec<(String, String, String)> = repos.iter()
|
let targets: Vec<(String, String, String)> = repos.iter()
|
||||||
.filter_map(|r| {
|
.filter_map(|r| {
|
||||||
let basename = r.path.file_name().map(|b| b.to_string_lossy().to_string())
|
let basename = r.remote_repo_name.clone().unwrap_or_else(|| {
|
||||||
.unwrap_or_else(|| r.name.clone());
|
r.path.file_name().map(|b| b.to_string_lossy().to_string()).unwrap_or_else(|| r.name.clone())
|
||||||
|
});
|
||||||
r.owner.as_ref().map(|o| (r.name.clone(), format!("{}/{}", o, basename), r.branch.clone()))
|
r.owner.as_ref().map(|o| (r.name.clone(), format!("{}/{}", o, basename), r.branch.clone()))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
@@ -718,14 +718,16 @@ pub fn spawn_api_refresh(state: &mut HubState) {
|
|||||||
Ok(a) if !a.is_empty() => a,
|
Ok(a) if !a.is_empty() => a,
|
||||||
_ => { state.status_msg = Some("No accounts configured — vulp auth add".into()); return; }
|
_ => { state.status_msg = Some("No accounts configured — vulp auth add".into()); return; }
|
||||||
};
|
};
|
||||||
// (display name = cache key, owner/basename slug for the API, branch for
|
// (display name = cache key, owner/repo slug for the API, branch for
|
||||||
// the CI-run lookup). Org-owned repos are reachable through the personal
|
// the CI-run lookup). Org-owned repos are reachable through the personal
|
||||||
// token, so no owner filtering.
|
// token, so no owner filtering. The repo name comes from the remote URL,
|
||||||
|
// not the local folder name — they can differ (vulp's own folder is
|
||||||
|
// "vulp", its GitHub repo is "fxg").
|
||||||
let repos: Vec<(String, Option<String>, String, String)> = state.repos.iter()
|
let repos: Vec<(String, Option<String>, String, String)> = state.repos.iter()
|
||||||
.map(|r| {
|
.map(|r| {
|
||||||
let basename = r.path.file_name()
|
let basename = r.remote_repo_name.clone().unwrap_or_else(|| {
|
||||||
.map(|b| b.to_string_lossy().to_string())
|
r.path.file_name().map(|b| b.to_string_lossy().to_string()).unwrap_or_else(|| r.name.clone())
|
||||||
.unwrap_or_else(|| r.name.clone());
|
});
|
||||||
(r.name.clone(), r.owner.clone(), basename, r.branch.clone())
|
(r.name.clone(), r.owner.clone(), basename, r.branch.clone())
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
+33
-1
@@ -29,6 +29,10 @@ pub struct RepoSummary {
|
|||||||
pub last_tag: Option<String>,
|
pub last_tag: Option<String>,
|
||||||
pub visibility: Option<String>,
|
pub visibility: Option<String>,
|
||||||
pub owner: Option<String>, // parsed from git remote URL
|
pub owner: Option<String>, // parsed from git remote URL
|
||||||
|
/// The provider-side repo name parsed from the git remote URL — not
|
||||||
|
/// necessarily the local folder name (see `parse_repo_name_from_url`).
|
||||||
|
/// Provider-API slugs must use this, never `path.file_name()`.
|
||||||
|
pub remote_repo_name: Option<String>,
|
||||||
pub has_license: bool,
|
pub has_license: bool,
|
||||||
pub lang: Option<String>,
|
pub lang: Option<String>,
|
||||||
pub bookmarked: bool,
|
pub bookmarked: bool,
|
||||||
@@ -94,12 +98,39 @@ fn parse_owner_from_url(url: &str) -> Option<String> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extracts the `repo` segment from an HTTPS or SSH git remote URL — the
|
||||||
|
/// name the provider actually knows the repo by, which is not necessarily
|
||||||
|
/// the local folder's name (a repo can be cloned into, or renamed to, any
|
||||||
|
/// directory name locally; `vulp`'s own local folder is "vulp" but its
|
||||||
|
/// GitHub repo is "fxg"). Callers building a provider-API slug must prefer
|
||||||
|
/// this over `path.file_name()`.
|
||||||
|
fn parse_repo_name_from_url(url: &str) -> Option<String> {
|
||||||
|
if url.starts_with("http") {
|
||||||
|
let trimmed = url.trim_end_matches('/').trim_end_matches(".git");
|
||||||
|
return trimmed.rsplit('/').next().map(|s| s.to_string());
|
||||||
|
}
|
||||||
|
// git@github.com:owner/repo.git
|
||||||
|
if let Some(colon) = url.rfind(':') {
|
||||||
|
let after = url[colon + 1..].trim_start_matches('/').trim_end_matches(".git");
|
||||||
|
return after.rsplit('/').next().map(|s| s.to_string());
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Parses the owner from `repo`'s `origin` remote, if it has one.
|
/// Parses the owner from `repo`'s `origin` remote, if it has one.
|
||||||
fn extract_owner(repo: &Repository) -> Option<String> {
|
fn extract_owner(repo: &Repository) -> Option<String> {
|
||||||
let remote = repo.find_remote("origin").ok()?;
|
let remote = repo.find_remote("origin").ok()?;
|
||||||
parse_owner_from_url(remote.url()?)
|
parse_owner_from_url(remote.url()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses the provider-side repo name from `repo`'s `origin` remote, if it
|
||||||
|
/// has one — see `parse_repo_name_from_url` for why this can differ from
|
||||||
|
/// the local folder name.
|
||||||
|
fn extract_remote_repo_name(repo: &Repository) -> Option<String> {
|
||||||
|
let remote = repo.find_remote("origin").ok()?;
|
||||||
|
parse_repo_name_from_url(remote.url()?)
|
||||||
|
}
|
||||||
|
|
||||||
// ── license + language detection ──────────────────────────────────────────────
|
// ── license + language detection ──────────────────────────────────────────────
|
||||||
|
|
||||||
/// Whether `path` has a recognizable license file at its root.
|
/// Whether `path` has a recognizable license file at its root.
|
||||||
@@ -372,6 +403,7 @@ pub fn repo_summary(path: &Path) -> Result<RepoSummary> {
|
|||||||
let tag_ms = t_tag.elapsed().as_millis();
|
let tag_ms = t_tag.elapsed().as_millis();
|
||||||
|
|
||||||
let owner = extract_owner(&repo);
|
let owner = extract_owner(&repo);
|
||||||
|
let remote_repo_name = extract_remote_repo_name(&repo);
|
||||||
let has_license = detect_license(path);
|
let has_license = detect_license(path);
|
||||||
|
|
||||||
let t_lang = std::time::Instant::now();
|
let t_lang = std::time::Instant::now();
|
||||||
@@ -399,7 +431,7 @@ pub fn repo_summary(path: &Path) -> Result<RepoSummary> {
|
|||||||
Ok(RepoSummary {
|
Ok(RepoSummary {
|
||||||
name, path: path.to_path_buf(), branch, ahead, behind, is_dirty,
|
name, path: path.to_path_buf(), branch, ahead, behind, is_dirty,
|
||||||
last_commit_msg: msg, last_commit_age: age, last_commit_ts: ts,
|
last_commit_msg: msg, last_commit_age: age, last_commit_ts: ts,
|
||||||
last_tag, visibility: None, owner, has_license, lang, bookmarked: false,
|
last_tag, visibility: None, owner, remote_repo_name, has_license, lang, bookmarked: false,
|
||||||
stars: None, open_issues: None, open_prs: None,
|
stars: None, open_issues: None, open_prs: None,
|
||||||
stash_count,
|
stash_count,
|
||||||
ci_status: None, dependabot_open: None, deploy_status: None,
|
ci_status: None, dependabot_open: None, deploy_status: None,
|
||||||
|
|||||||
Reference in new issue
Block a user