Fix git-tag cache-key reader in case of slashes (#10467) (#10500)

## Summary

The assumption that all tags are listed under a flat `.git/ref/tags`
structure was wrong. Git creates a hierarchy of directories for tags
containing slashes. To fix the cache key calculation, we need to
recursively traverse all files under that folder instead.

## Test Plan

1. Create an `uv` project with git-tag cache-keys;
2. Add any tag with slash;
3. Run `uv sync` and see uv_cache_info error in verbose log;
4. `uv sync` doesn't trigger reinstall on next tag addition or removal;
5. With fix applied, reinstall triggers on every tag update and there
are no errors in the log.

Fixes #10467

---------

Co-authored-by: Sergei Nizovtsev <sergei.nizovtsev@eqvilent.com>
Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
This commit is contained in:
Sergei Nizovtsev 2025-01-12 05:30:46 +03:00 committed by GitHub
parent 7269273458
commit 051aaa5fe5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 21 additions and 13 deletions

1
Cargo.lock generated
View File

@ -4739,6 +4739,7 @@ dependencies = [
"thiserror 2.0.11", "thiserror 2.0.11",
"toml", "toml",
"tracing", "tracing",
"walkdir",
] ]
[[package]] [[package]]

View File

@ -23,3 +23,4 @@ serde = { workspace = true, features = ["derive"] }
thiserror = { workspace = true } thiserror = { workspace = true }
toml = { workspace = true } toml = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
walkdir = { workspace = true }

View File

@ -1,6 +1,9 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tracing::warn;
use walkdir::WalkDir;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub(crate) enum GitInfoError { pub(crate) enum GitInfoError {
#[error("The repository at {0} is missing a `.git` directory")] #[error("The repository at {0} is missing a `.git` directory")]
@ -80,24 +83,27 @@ impl Tags {
.find(|git_dir| git_dir.exists()) .find(|git_dir| git_dir.exists())
.ok_or_else(|| GitInfoError::MissingGitDir(path.to_path_buf()))?; .ok_or_else(|| GitInfoError::MissingGitDir(path.to_path_buf()))?;
let git_refs_path = let git_tags_path = git_refs(&git_dir)
git_refs(&git_dir).ok_or_else(|| GitInfoError::MissingRefs(git_dir.clone()))?; .ok_or_else(|| GitInfoError::MissingRefs(git_dir.clone()))?
.join("tags");
let mut tags = BTreeMap::new(); let mut tags = BTreeMap::new();
// Map each tag to its commit. // Map each tag to its commit.
let read_dir = match fs_err::read_dir(git_refs_path.join("tags")) { for entry in WalkDir::new(&git_tags_path).contents_first(true) {
Ok(read_dir) => read_dir, let entry = match entry {
Err(err) if err.kind() == std::io::ErrorKind::NotFound => { Ok(entry) => entry,
return Ok(Self(tags)); Err(err) => {
} warn!("Failed to read Git tags: {err}");
Err(err) => return Err(err.into()), continue;
}; }
for entry in read_dir { };
let entry = entry?;
let path = entry.path(); let path = entry.path();
if let Some(tag) = path.file_name().and_then(|name| name.to_str()) { if !entry.file_type().is_file() {
let commit = fs_err::read_to_string(&path)?.trim().to_string(); continue;
}
if let Ok(Some(tag)) = path.strip_prefix(&git_tags_path).map(|name| name.to_str()) {
let commit = fs_err::read_to_string(path)?.trim().to_string();
// The commit should be 40 hexadecimal characters. // The commit should be 40 hexadecimal characters.
if commit.len() != 40 { if commit.len() != 40 {