Skip to content

Commit b1fbb52

Browse files
Include SHA when listing lockfile changes (#15817)
## Summary Right now, we only list changes if the _version_ differs. This PR takes the SHA into account. We may want to list changes to _any_ sources, but that gets more complicated (e.g., if the user swaps the index URL, we'd have to show _all_ changes to the index URL). Closes #15810.
1 parent bd8a934 commit b1fbb52

File tree

4 files changed

+172
-28
lines changed

4 files changed

+172
-28
lines changed

crates/uv-git-types/src/oid.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ impl GitOid {
2525
pub fn as_short_str(&self) -> &str {
2626
&self.as_str()[..16]
2727
}
28+
29+
/// Return a (very) truncated representation, i.e., the first 8 characters of the SHA.
30+
pub fn as_tiny_str(&self) -> &str {
31+
&self.as_str()[..8]
32+
}
2833
}
2934

3035
#[derive(Debug, Error, PartialEq)]

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 Git SHA of the package, if it is a Git source.
3062+
pub fn git_sha(&self) -> Option<&GitOid> {
3063+
match &self.id.source {
3064+
Source::Git(_, git) => Some(&git.precise),
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: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use uv_distribution_types::{
2222
Requirement, RequiresPython, UnresolvedRequirementSpecification,
2323
};
2424
use uv_git::ResolvedRepositoryReference;
25+
use uv_git_types::GitOid;
2526
use uv_normalize::{GroupName, PackageName};
2627
use uv_pep440::Version;
2728
use uv_preview::{Preview, PreviewFeatures};
@@ -30,7 +31,7 @@ use uv_python::{Interpreter, PythonDownloads, PythonEnvironment, PythonPreferenc
3031
use uv_requirements::ExtrasResolver;
3132
use uv_requirements::upgrade::{LockedRequirements, read_lock_requirements};
3233
use uv_resolver::{
33-
FlatIndex, InMemoryIndex, Lock, Options, OptionsBuilder, PythonRequirement,
34+
FlatIndex, InMemoryIndex, Lock, Options, OptionsBuilder, Package, PythonRequirement,
3435
ResolverEnvironment, ResolverManifest, SatisfiesResult, UniversalMarker,
3536
};
3637
use uv_scripts::Pep723Script;
@@ -1355,28 +1356,56 @@ impl ValidatedLock {
13551356
}
13561357
}
13571358

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

13711400
impl<'lock> LockEvent<'lock> {
13721401
/// Detect the change events between an (optional) existing and updated lockfile.
1373-
pub(crate) fn detect_changes(
1402+
fn detect_changes(
13741403
existing_lock: Option<&'lock Lock>,
13751404
new_lock: &'lock Lock,
13761405
dry_run: DryRun,
13771406
) -> impl Iterator<Item = Self> {
13781407
// Identify the package-versions in the existing lockfile.
1379-
let mut existing_packages: FxHashMap<&PackageName, BTreeSet<Option<&Version>>> =
1408+
let mut existing_packages: FxHashMap<&PackageName, BTreeSet<LockEventVersion>> =
13801409
if let Some(existing_lock) = existing_lock {
13811410
existing_lock.packages().iter().fold(
13821411
FxHashMap::with_capacity_and_hasher(
@@ -1386,7 +1415,7 @@ impl<'lock> LockEvent<'lock> {
13861415
|mut acc, package| {
13871416
acc.entry(package.name())
13881417
.or_default()
1389-
.insert(package.version());
1418+
.insert(LockEventVersion::from(package));
13901419
acc
13911420
},
13921421
)
@@ -1395,13 +1424,13 @@ impl<'lock> LockEvent<'lock> {
13951424
};
13961425

13971426
// Identify the package-versions in the updated lockfile.
1398-
let mut new_packages: FxHashMap<&PackageName, BTreeSet<Option<&Version>>> =
1427+
let mut new_packages: FxHashMap<&PackageName, BTreeSet<LockEventVersion>> =
13991428
new_lock.packages().iter().fold(
14001429
FxHashMap::with_capacity_and_hasher(new_lock.packages().len(), FxBuildHasher),
14011430
|mut acc, package| {
14021431
acc.entry(package.name())
14031432
.or_default()
1404-
.insert(package.version());
1433+
.insert(LockEventVersion::from(package));
14051434
acc
14061435
},
14071436
);
@@ -1435,23 +1464,16 @@ impl<'lock> LockEvent<'lock> {
14351464

14361465
impl std::fmt::Display for LockEvent<'_> {
14371466
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-
14451467
match self {
14461468
Self::Update(dry_run, name, existing_versions, new_versions) => {
14471469
let existing_versions = existing_versions
14481470
.iter()
1449-
.map(|version| format_version(*version))
1471+
.map(std::string::ToString::to_string)
14501472
.collect::<Vec<_>>()
14511473
.join(", ");
14521474
let new_versions = new_versions
14531475
.iter()
1454-
.map(|version| format_version(*version))
1476+
.map(std::string::ToString::to_string)
14551477
.collect::<Vec<_>>()
14561478
.join(", ");
14571479

@@ -1470,7 +1492,7 @@ impl std::fmt::Display for LockEvent<'_> {
14701492
Self::Add(dry_run, name, new_versions) => {
14711493
let new_versions = new_versions
14721494
.iter()
1473-
.map(|version| format_version(*version))
1495+
.map(std::string::ToString::to_string)
14741496
.collect::<Vec<_>>()
14751497
.join(", ");
14761498

@@ -1485,7 +1507,7 @@ impl std::fmt::Display for LockEvent<'_> {
14851507
Self::Remove(dry_run, name, existing_versions) => {
14861508
let existing_versions = existing_versions
14871509
.iter()
1488-
.map(|version| format_version(*version))
1510+
.map(std::string::ToString::to_string)
14891511
.collect::<Vec<_>>()
14901512
.join(", ");
14911513

crates/uv/tests/it/lock.rs

Lines changed: 115 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -407,14 +407,15 @@ fn lock_sdist_git() -> Result<()> {
407407
"#,
408408
)?;
409409

410-
uv_snapshot!(context.filters(), context.lock(), @r###"
410+
uv_snapshot!(context.filters(), context.lock(), @r"
411411
success: true
412412
exit_code: 0
413413
----- stdout -----
414414

415415
----- stderr -----
416416
Resolved 2 packages in [TIME]
417-
"###);
417+
Updated uv-public-pypackage v0.1.0 (0dacfd66) -> v0.1.0 (b270df1a)
418+
");
418419

419420
let lock = context.read("uv.lock");
420421

@@ -738,14 +739,15 @@ fn lock_sdist_git_pep508() -> Result<()> {
738739
"#,
739740
)?;
740741

741-
uv_snapshot!(context.filters(), context.lock(), @r###"
742+
uv_snapshot!(context.filters(), context.lock(), @r"
742743
success: true
743744
exit_code: 0
744745
----- stdout -----
745746

746747
----- stderr -----
747748
Resolved 2 packages in [TIME]
748-
"###);
749+
Updated uv-public-pypackage v0.1.0 (0dacfd66) -> v0.1.0 (b270df1a)
750+
");
749751

750752
let lock = context.read("uv.lock");
751753

@@ -4942,6 +4944,7 @@ fn lock_git_sha() -> Result<()> {
49424944

49434945
----- stderr -----
49444946
Resolved 2 packages in [TIME]
4947+
Updated uv-public-pypackage v0.1.0 (0dacfd66) -> v0.1.0 (b270df1a)
49454948
");
49464949

49474950
let lock = context.read("uv.lock");
@@ -12937,14 +12940,15 @@ fn lock_mismatched_sources() -> Result<()> {
1293712940
});
1293812941

1293912942
// If we run with `--no-sources`, we should use the URL provided in `project.dependencies`.
12940-
uv_snapshot!(context.filters(), context.lock().arg("--no-sources"), @r###"
12943+
uv_snapshot!(context.filters(), context.lock().arg("--no-sources"), @r"
1294112944
success: true
1294212945
exit_code: 0
1294312946
----- stdout -----
1294412947

1294512948
----- stderr -----
1294612949
Resolved 2 packages in [TIME]
12947-
"###);
12950+
Updated uv-public-pypackage v0.1.0 (0dacfd66) -> v0.1.0 (b270df1a)
12951+
");
1294812952

1294912953
let lock = context.read("uv.lock");
1295012954

@@ -31519,6 +31523,111 @@ fn lock_android() -> Result<()> {
3151931523
Ok(())
3152031524
}
3152131525

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

0 commit comments

Comments
 (0)