Skip to content

Commit 2a2022d

Browse files
committed
Include SHA when listing lockfile changes
1 parent 9153d1a commit 2a2022d

File tree

3 files changed

+156
-22
lines changed

3 files changed

+156
-22
lines changed

crates/uv-resolver/src/lock/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3058,6 +3058,14 @@ impl Package {
30583058
self.id.version.as_ref()
30593059
}
30603060

3061+
/// Returns the short Git SHA of the package, if it is a Git source.
3062+
pub fn short_git_sha(&self) -> Option<&str> {
3063+
match &self.id.source {
3064+
Source::Git(_, git) => Some(git.precise.as_short_str()),
3065+
_ => None,
3066+
}
3067+
}
3068+
30613069
/// Return the fork markers for this package, if any.
30623070
pub fn fork_markers(&self) -> &[UniversalMarker] {
30633071
self.fork_markers.as_slice()

crates/uv/src/commands/project/lock.rs

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ use uv_python::{Interpreter, PythonDownloads, PythonEnvironment, PythonPreferenc
3030
use uv_requirements::ExtrasResolver;
3131
use uv_requirements::upgrade::{LockedRequirements, read_lock_requirements};
3232
use uv_resolver::{
33-
FlatIndex, InMemoryIndex, Lock, Options, OptionsBuilder, PythonRequirement,
33+
FlatIndex, InMemoryIndex, Lock, Options, OptionsBuilder, Package, PythonRequirement,
3434
ResolverEnvironment, ResolverManifest, SatisfiesResult, UniversalMarker,
3535
};
3636
use uv_scripts::Pep723Script;
@@ -1355,28 +1355,56 @@ impl ValidatedLock {
13551355
}
13561356
}
13571357

1358+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1359+
struct LockEventVersion<'lock> {
1360+
/// The version of the package, or `None` if the package has a dynamic version.
1361+
version: Option<&'lock Version>,
1362+
/// The short Git SHA of the package, if it was installed from a Git repository.
1363+
sha: Option<&'lock str>,
1364+
}
1365+
1366+
impl<'lock> From<&'lock Package> for LockEventVersion<'lock> {
1367+
fn from(value: &'lock Package) -> Self {
1368+
Self {
1369+
version: value.version(),
1370+
sha: value.short_git_sha(),
1371+
}
1372+
}
1373+
}
1374+
1375+
impl std::fmt::Display for LockEventVersion<'_> {
1376+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1377+
match (self.version, self.sha) {
1378+
(Some(version), Some(sha)) => write!(f, "v{version} ({sha})"),
1379+
(Some(version), None) => write!(f, "v{version}"),
1380+
(None, Some(sha)) => write!(f, "(dynamic) ({sha})"),
1381+
(None, None) => write!(f, "(dynamic)"),
1382+
}
1383+
}
1384+
}
1385+
13581386
/// A modification to a lockfile.
13591387
#[derive(Debug, Clone)]
1360-
pub(crate) enum LockEvent<'lock> {
1388+
enum LockEvent<'lock> {
13611389
Update(
13621390
DryRun,
13631391
PackageName,
1364-
BTreeSet<Option<&'lock Version>>,
1365-
BTreeSet<Option<&'lock Version>>,
1392+
BTreeSet<LockEventVersion<'lock>>,
1393+
BTreeSet<LockEventVersion<'lock>>,
13661394
),
1367-
Add(DryRun, PackageName, BTreeSet<Option<&'lock Version>>),
1368-
Remove(DryRun, PackageName, BTreeSet<Option<&'lock Version>>),
1395+
Add(DryRun, PackageName, BTreeSet<LockEventVersion<'lock>>),
1396+
Remove(DryRun, PackageName, BTreeSet<LockEventVersion<'lock>>),
13691397
}
13701398

13711399
impl<'lock> LockEvent<'lock> {
13721400
/// Detect the change events between an (optional) existing and updated lockfile.
1373-
pub(crate) fn detect_changes(
1401+
fn detect_changes(
13741402
existing_lock: Option<&'lock Lock>,
13751403
new_lock: &'lock Lock,
13761404
dry_run: DryRun,
13771405
) -> impl Iterator<Item = Self> {
13781406
// Identify the package-versions in the existing lockfile.
1379-
let mut existing_packages: FxHashMap<&PackageName, BTreeSet<Option<&Version>>> =
1407+
let mut existing_packages: FxHashMap<&PackageName, BTreeSet<LockEventVersion>> =
13801408
if let Some(existing_lock) = existing_lock {
13811409
existing_lock.packages().iter().fold(
13821410
FxHashMap::with_capacity_and_hasher(
@@ -1386,7 +1414,7 @@ impl<'lock> LockEvent<'lock> {
13861414
|mut acc, package| {
13871415
acc.entry(package.name())
13881416
.or_default()
1389-
.insert(package.version());
1417+
.insert(LockEventVersion::from(package));
13901418
acc
13911419
},
13921420
)
@@ -1395,13 +1423,13 @@ impl<'lock> LockEvent<'lock> {
13951423
};
13961424

13971425
// Identify the package-versions in the updated lockfile.
1398-
let mut new_packages: FxHashMap<&PackageName, BTreeSet<Option<&Version>>> =
1426+
let mut new_packages: FxHashMap<&PackageName, BTreeSet<LockEventVersion>> =
13991427
new_lock.packages().iter().fold(
14001428
FxHashMap::with_capacity_and_hasher(new_lock.packages().len(), FxBuildHasher),
14011429
|mut acc, package| {
14021430
acc.entry(package.name())
14031431
.or_default()
1404-
.insert(package.version());
1432+
.insert(LockEventVersion::from(package));
14051433
acc
14061434
},
14071435
);
@@ -1435,23 +1463,16 @@ impl<'lock> LockEvent<'lock> {
14351463

14361464
impl std::fmt::Display for LockEvent<'_> {
14371465
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1438-
/// Format a version for inclusion in the upgrade report.
1439-
fn format_version(version: Option<&Version>) -> String {
1440-
version
1441-
.map(|version| format!("v{version}"))
1442-
.unwrap_or_else(|| "(dynamic)".to_string())
1443-
}
1444-
14451466
match self {
14461467
Self::Update(dry_run, name, existing_versions, new_versions) => {
14471468
let existing_versions = existing_versions
14481469
.iter()
1449-
.map(|version| format_version(*version))
1470+
.map(std::string::ToString::to_string)
14501471
.collect::<Vec<_>>()
14511472
.join(", ");
14521473
let new_versions = new_versions
14531474
.iter()
1454-
.map(|version| format_version(*version))
1475+
.map(std::string::ToString::to_string)
14551476
.collect::<Vec<_>>()
14561477
.join(", ");
14571478

@@ -1470,7 +1491,7 @@ impl std::fmt::Display for LockEvent<'_> {
14701491
Self::Add(dry_run, name, new_versions) => {
14711492
let new_versions = new_versions
14721493
.iter()
1473-
.map(|version| format_version(*version))
1494+
.map(std::string::ToString::to_string)
14741495
.collect::<Vec<_>>()
14751496
.join(", ");
14761497

@@ -1485,7 +1506,7 @@ impl std::fmt::Display for LockEvent<'_> {
14851506
Self::Remove(dry_run, name, existing_versions) => {
14861507
let existing_versions = existing_versions
14871508
.iter()
1488-
.map(|version| format_version(*version))
1509+
.map(std::string::ToString::to_string)
14891510
.collect::<Vec<_>>()
14901511
.join(", ");
14911512

crates/uv/tests/it/lock.rs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31519,6 +31519,111 @@ fn lock_android() -> Result<()> {
3151931519
Ok(())
3152031520
}
3152131521

31522+
/// See: <https://github.com/astral-sh/uv/issues/9832#issuecomment-2539121761>
31523+
#[test]
31524+
fn lock_git_change_log() -> Result<()> {
31525+
let context = TestContext::new("3.12");
31526+
31527+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
31528+
pyproject_toml.write_str(
31529+
r#"
31530+
[project]
31531+
name = "foo"
31532+
version = "0.1.0"
31533+
requires-python = ">=3.12.0"
31534+
dependencies = [
31535+
"typing-extensions",
31536+
]
31537+
31538+
[tool.uv.sources]
31539+
typing-extensions = { git = "https://github.com/python/typing_extensions" }
31540+
"#,
31541+
)?;
31542+
31543+
// Write a stale commit.
31544+
context.temp_dir.child("uv.lock").write_str(
31545+
r#"
31546+
version = 1
31547+
revision = 3
31548+
requires-python = ">=3.12.0"
31549+
31550+
[options]
31551+
exclude-newer = "2024-03-25T00:00:00Z"
31552+
31553+
[[package]]
31554+
name = "foo"
31555+
version = "0.1.0"
31556+
source = { virtual = "." }
31557+
dependencies = [
31558+
{ name = "typing-extensions" },
31559+
]
31560+
31561+
[package.metadata]
31562+
requires-dist = [{ name = "typing-extensions", git = "https://github.com/python/typing_extensions?rev=4f42e6bf0052129bc6dae5e71699a409652d2091" }]
31563+
31564+
[[package]]
31565+
name = "typing-extensions"
31566+
version = "4.15.0"
31567+
source = { git = "https://github.com/python/typing_extensions?rev=4f42e6bf0052129bc6dae5e71699a409652d2091#4f42e6bf0052129bc6dae5e71699a409652d2091" }
31568+
"#,
31569+
)?;
31570+
31571+
uv_snapshot!(context.filters(), context.lock().arg("--dry-run"), @r"
31572+
success: true
31573+
exit_code: 0
31574+
----- stdout -----
31575+
31576+
----- stderr -----
31577+
Resolved 2 packages in [TIME]
31578+
Update typing-extensions v4.15.0 (4f42e6bf0052129b) -> v4.15.0 (9215c953610ca4e4)
31579+
");
31580+
31581+
uv_snapshot!(context.filters(), context.lock(), @r"
31582+
success: true
31583+
exit_code: 0
31584+
----- stdout -----
31585+
31586+
----- stderr -----
31587+
Resolved 2 packages in [TIME]
31588+
Updated typing-extensions v4.15.0 (4f42e6bf0052129b) -> v4.15.0 (9215c953610ca4e4)
31589+
");
31590+
31591+
let lock = context.read("uv.lock");
31592+
31593+
insta::with_settings!({
31594+
filters => context.filters(),
31595+
}, {
31596+
assert_snapshot!(
31597+
lock, @r#"
31598+
version = 1
31599+
revision = 3
31600+
requires-python = ">=3.12.[X]"
31601+
31602+
[options]
31603+
exclude-newer = "2024-03-25T00:00:00Z"
31604+
31605+
[[package]]
31606+
name = "foo"
31607+
version = "0.1.0"
31608+
source = { virtual = "." }
31609+
dependencies = [
31610+
{ name = "typing-extensions" },
31611+
]
31612+
31613+
[package.metadata]
31614+
requires-dist = [{ name = "typing-extensions", git = "https://github.com/python/typing_extensions" }]
31615+
31616+
[[package]]
31617+
name = "typing-extensions"
31618+
version = "4.15.0"
31619+
source = { git = "https://github.com/python/typing_extensions#9215c953610ca4e4ce7ae840a0a804505da70a05" }
31620+
"#
31621+
);
31622+
});
31623+
31624+
Ok(())
31625+
}
31626+
3152231627
#[test]
3152331628
fn lock_required_intersection() -> Result<()> {
3152431629
let context = TestContext::new("3.12");

0 commit comments

Comments
 (0)