upd-cli 0.1.2__tar.gz → 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {upd_cli-0.1.2 → upd_cli-0.1.3}/CHANGELOG.md +16 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/Cargo.lock +1 -1
- {upd_cli-0.1.2 → upd_cli-0.1.3}/Cargo.toml +1 -1
- {upd_cli-0.1.2 → upd_cli-0.1.3}/PKG-INFO +32 -2
- {upd_cli-0.1.2 → upd_cli-0.1.3}/README.md +31 -1
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/audit/mod.rs +38 -39
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/cli.rs +3 -2
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/cooldown.rs +1 -40
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/lockfile.rs +174 -71
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/main.rs +48 -5
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/updater/mod.rs +2 -0
- upd_cli-0.1.3/src/updater/npm_range.rs +257 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/updater/package_json.rs +432 -17
- upd_cli-0.1.3/src/version/compare.rs +79 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/version/mod.rs +1 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/.mise.toml +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/.pre-commit-config.yaml +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/.pre-commit-hooks.yaml +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/.rumdl.toml +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/LICENSE +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/Makefile +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/assets/logo-wide.svg +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/assets/logo.svg +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/pyproject.toml +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/python/upd_cli/__init__.py +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/python/upd_cli/__main__.py +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/python/upd_cli/py.typed +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/rust-toolchain.toml +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/align.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/audit/cache.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/audit/cvss.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/cache.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/config.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/interactive.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/lib.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/output.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/registry/crates_io.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/registry/github_releases.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/registry/go_proxy.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/registry/mock.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/registry/mod.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/registry/npm.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/registry/nuget.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/registry/pypi.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/registry/rubygems.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/registry/terraform.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/registry/utils.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/updater/cargo_toml.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/updater/csproj.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/updater/gemfile.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/updater/github_actions.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/updater/go_mod.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/updater/mise.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/updater/pre_commit.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/updater/pyproject.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/updater/requirements.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/updater/terraform.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/version/pep440.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/version/semver_util.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/src/version/tag.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/tests/audit_offline.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/tests/audit_sarif.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/tests/audit_severity.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/tests/bump_filter.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/tests/cooldown_e2e.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/tests/exit_codes.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/tests/fix_audit.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/tests/format_json.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/tests/help_text.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/tests/interactive_tty.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/tests/invalid_positional.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/tests/no_args_scope.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/tests/output_streams.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/tests/package_filter.rs +0 -0
- {upd_cli-0.1.2 → upd_cli-0.1.3}/vership.toml +0 -0
|
@@ -12,6 +12,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
|
|
16
|
+
## [0.1.3](https://github.com/rvben/upd/compare/v0.1.2...v0.1.3) - 2026-04-25
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **lock**: scope lockfile regeneration to the packages upd actually changed ([6b6cfa6](https://github.com/rvben/upd/commit/6b6cfa6fe3e8e8d787e78c7afea24883ebe67833))
|
|
21
|
+
- **npm**: classify and rewrite comparator-range specs ([a60979a](https://github.com/rvben/upd/commit/a60979acd7edb7eb6adfac554a590412a6cf8271))
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **lock**: include config pins in targeted regenerate and update CLI help ([207fdf9](https://github.com/rvben/upd/commit/207fdf94af95c74865f828f258ec9cfa61d22085))
|
|
26
|
+
- **npm**: preserve upper bound when pinning comparator-range specs ([fb7c863](https://github.com/rvben/upd/commit/fb7c863de9f07201af310ab13a59696d17fcb766))
|
|
27
|
+
- **npm**: apply config policy and cooldown to comparator-range updates ([be095dd](https://github.com/rvben/upd/commit/be095dd30443dd547fb6913dae551cbd530cb019))
|
|
28
|
+
- **npm**: update comparator-range specs via constraint-aware resolution ([4481b8b](https://github.com/rvben/upd/commit/4481b8bad6a02c6f761cf489a547b5546099e871))
|
|
29
|
+
- **audit**: order fix versions numerically, not lexicographically ([069eb1d](https://github.com/rvben/upd/commit/069eb1dc8771640376957339e40ada7b60a82b0a))
|
|
30
|
+
|
|
15
31
|
## [0.1.2](https://github.com/rvben/upd/compare/v0.1.1...v0.1.2) - 2026-04-24
|
|
16
32
|
|
|
17
33
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: upd-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Environment :: Console
|
|
6
6
|
Classifier: Intended Audience :: Developers
|
|
@@ -46,7 +46,10 @@ pipx run --spec upd-cli upd --apply
|
|
|
46
46
|
|
|
47
47
|
- **Multi-ecosystem**: Python, Node.js, Rust, Go, Ruby, .NET, Terraform, GitHub Actions, pre-commit, Mise/asdf
|
|
48
48
|
- **Fast**: Parallel registry requests for all dependencies
|
|
49
|
-
- **Constraint-aware**: Respects
|
|
49
|
+
- **Constraint-aware**: Respects `>=2.0,<3` (Python), `~> 7.1` (Ruby), and `^2.0.0` / `~2.0.0` (npm, Cargo).
|
|
50
|
+
For npm, comparator ranges such as `">=1.0.0 <2.0.0"` are rewritten with a **bump strategy**: the lower
|
|
51
|
+
bound moves to the highest version satisfying the constraint, preserving the upper bound. Hyphen
|
|
52
|
+
(`"1 - 2"`) and OR (`"^1 || ^2"`) ranges are reported as warnings and left untouched.
|
|
50
53
|
- **Smart caching**: 24-hour version cache for faster subsequent runs
|
|
51
54
|
- **Update filters**: Filter by bump level with `--only-bump <major|minor|patch>` (repeatable) or cap with `--max-bump`
|
|
52
55
|
- **Interactive mode**: Approve updates individually with `-i`
|
|
@@ -767,6 +770,33 @@ Global flags (accepted on every subcommand):
|
|
|
767
770
|
|
|
768
771
|
Subcommands: `update` (default), `align`, `audit`, `clean-cache`, `self-update`.
|
|
769
772
|
|
|
773
|
+
#### Commands run by `--lock`
|
|
774
|
+
|
|
775
|
+
`upd --lock` runs the narrowest per-ecosystem refresh command that
|
|
776
|
+
updates only the packages `upd` just rewrote. Targeted forms are used
|
|
777
|
+
wherever the package manager supports them; targeting falls back to
|
|
778
|
+
`--lockfile-only` flags where no per-package form exists; otherwise
|
|
779
|
+
the manifest-wide refresh command is used.
|
|
780
|
+
|
|
781
|
+
| Ecosystem | Lockfile | Command |
|
|
782
|
+
|-----------|--------------------------|------------------------------------------------|
|
|
783
|
+
| Python | `poetry.lock` | `poetry lock --no-update` |
|
|
784
|
+
| Python | `uv.lock` | `uv lock` |
|
|
785
|
+
| Node | `package-lock.json` | `npm install --package-lock-only` |
|
|
786
|
+
| Node | `yarn.lock` | `yarn install --mode update-lockfile` (Yarn 2+)|
|
|
787
|
+
| Node | `pnpm-lock.yaml` | `pnpm install --lockfile-only` |
|
|
788
|
+
| Node | `bun.lockb` | `bun install` |
|
|
789
|
+
| Rust | `Cargo.lock` | `cargo update -p <changed> -p <changed> …` |
|
|
790
|
+
| Go | `go.sum` | `go mod tidy` (no targeted form) |
|
|
791
|
+
| Ruby | `Gemfile.lock` | `bundle lock --update <changed> …` |
|
|
792
|
+
| .NET | `packages.lock.json` | `dotnet restore` (no targeted form) |
|
|
793
|
+
| Terraform | `.terraform.lock.hcl` | `terraform providers lock` (no targeted form) |
|
|
794
|
+
|
|
795
|
+
Manifests whose `upd` pass produced zero changes have their lockfile
|
|
796
|
+
refresh skipped entirely. A directory where only config pins were
|
|
797
|
+
applied is still refreshed, and the changed-package list includes
|
|
798
|
+
those pinned packages so `cargo update -p <pkg>` / `bundle lock --update <pkg>` stay scoped.
|
|
799
|
+
|
|
770
800
|
Stable `audit`-specific flags:
|
|
771
801
|
|
|
772
802
|
| Flag | Purpose |
|
|
@@ -23,7 +23,10 @@ pipx run --spec upd-cli upd --apply
|
|
|
23
23
|
|
|
24
24
|
- **Multi-ecosystem**: Python, Node.js, Rust, Go, Ruby, .NET, Terraform, GitHub Actions, pre-commit, Mise/asdf
|
|
25
25
|
- **Fast**: Parallel registry requests for all dependencies
|
|
26
|
-
- **Constraint-aware**: Respects
|
|
26
|
+
- **Constraint-aware**: Respects `>=2.0,<3` (Python), `~> 7.1` (Ruby), and `^2.0.0` / `~2.0.0` (npm, Cargo).
|
|
27
|
+
For npm, comparator ranges such as `">=1.0.0 <2.0.0"` are rewritten with a **bump strategy**: the lower
|
|
28
|
+
bound moves to the highest version satisfying the constraint, preserving the upper bound. Hyphen
|
|
29
|
+
(`"1 - 2"`) and OR (`"^1 || ^2"`) ranges are reported as warnings and left untouched.
|
|
27
30
|
- **Smart caching**: 24-hour version cache for faster subsequent runs
|
|
28
31
|
- **Update filters**: Filter by bump level with `--only-bump <major|minor|patch>` (repeatable) or cap with `--max-bump`
|
|
29
32
|
- **Interactive mode**: Approve updates individually with `-i`
|
|
@@ -744,6 +747,33 @@ Global flags (accepted on every subcommand):
|
|
|
744
747
|
|
|
745
748
|
Subcommands: `update` (default), `align`, `audit`, `clean-cache`, `self-update`.
|
|
746
749
|
|
|
750
|
+
#### Commands run by `--lock`
|
|
751
|
+
|
|
752
|
+
`upd --lock` runs the narrowest per-ecosystem refresh command that
|
|
753
|
+
updates only the packages `upd` just rewrote. Targeted forms are used
|
|
754
|
+
wherever the package manager supports them; targeting falls back to
|
|
755
|
+
`--lockfile-only` flags where no per-package form exists; otherwise
|
|
756
|
+
the manifest-wide refresh command is used.
|
|
757
|
+
|
|
758
|
+
| Ecosystem | Lockfile | Command |
|
|
759
|
+
|-----------|--------------------------|------------------------------------------------|
|
|
760
|
+
| Python | `poetry.lock` | `poetry lock --no-update` |
|
|
761
|
+
| Python | `uv.lock` | `uv lock` |
|
|
762
|
+
| Node | `package-lock.json` | `npm install --package-lock-only` |
|
|
763
|
+
| Node | `yarn.lock` | `yarn install --mode update-lockfile` (Yarn 2+)|
|
|
764
|
+
| Node | `pnpm-lock.yaml` | `pnpm install --lockfile-only` |
|
|
765
|
+
| Node | `bun.lockb` | `bun install` |
|
|
766
|
+
| Rust | `Cargo.lock` | `cargo update -p <changed> -p <changed> …` |
|
|
767
|
+
| Go | `go.sum` | `go mod tidy` (no targeted form) |
|
|
768
|
+
| Ruby | `Gemfile.lock` | `bundle lock --update <changed> …` |
|
|
769
|
+
| .NET | `packages.lock.json` | `dotnet restore` (no targeted form) |
|
|
770
|
+
| Terraform | `.terraform.lock.hcl` | `terraform providers lock` (no targeted form) |
|
|
771
|
+
|
|
772
|
+
Manifests whose `upd` pass produced zero changes have their lockfile
|
|
773
|
+
refresh skipped entirely. A directory where only config pins were
|
|
774
|
+
applied is still refreshed, and the changed-package list includes
|
|
775
|
+
those pinned packages so `cargo update -p <pkg>` / `bundle lock --update <pkg>` stay scoped.
|
|
776
|
+
|
|
747
777
|
Stable `audit`-specific flags:
|
|
748
778
|
|
|
749
779
|
| Flag | Purpose |
|
|
@@ -134,7 +134,7 @@ pub fn compute_fix_plan(audit: &AuditResult) -> (HashMap<String, String>, Vec<(S
|
|
|
134
134
|
.vulnerabilities
|
|
135
135
|
.iter()
|
|
136
136
|
.filter_map(|v| v.fixed_version.as_deref())
|
|
137
|
-
.max_by(|a, b|
|
|
137
|
+
.max_by(|a, b| crate::version::compare::compare_versions(a, b));
|
|
138
138
|
|
|
139
139
|
if let Some(version) = max_fixed {
|
|
140
140
|
fixable.insert(name.clone(), version.to_string());
|
|
@@ -144,44 +144,6 @@ pub fn compute_fix_plan(audit: &AuditResult) -> (HashMap<String, String>, Vec<(S
|
|
|
144
144
|
(fixable, unfixable)
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
/// Compare two version strings for ordering, preferring semver but falling back
|
|
148
|
-
/// to lexicographic comparison for non-semver ecosystems.
|
|
149
|
-
fn compare_fix_versions(a: &str, b: &str) -> std::cmp::Ordering {
|
|
150
|
-
match (semver_parse(a), semver_parse(b)) {
|
|
151
|
-
(Some(va), Some(vb)) => va.cmp(&vb),
|
|
152
|
-
_ => a.cmp(b),
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/// Parse a version string as semver, accepting an optional leading `v`.
|
|
157
|
-
///
|
|
158
|
-
/// Returns `(major, minor, patch, is_stable)` where `is_stable` is 1 for stable
|
|
159
|
-
/// releases and 0 for pre-releases (e.g. `2.0.0-rc1`). This ensures stable
|
|
160
|
-
/// versions beat pre-releases when all numeric components are equal.
|
|
161
|
-
fn semver_parse(v: &str) -> Option<(u64, u64, u64, u8)> {
|
|
162
|
-
let v = v.trim_start_matches('v');
|
|
163
|
-
let parts: Vec<&str> = v.split('.').collect();
|
|
164
|
-
if parts.len() < 2 {
|
|
165
|
-
return None;
|
|
166
|
-
}
|
|
167
|
-
let major: u64 = parts[0].parse().ok()?;
|
|
168
|
-
let minor: u64 = parts[1].parse().ok()?;
|
|
169
|
-
// Patch may carry a pre-release or build suffix (e.g. "0-rc1", "3+build1").
|
|
170
|
-
// Take only the leading digit run so "0-rc1" → patch=0, rest="-rc1".
|
|
171
|
-
let patch_part = parts.get(2).copied().unwrap_or("0");
|
|
172
|
-
let (patch_digits, rest) = patch_part
|
|
173
|
-
.split_once(|c: char| !c.is_ascii_digit())
|
|
174
|
-
.unwrap_or((patch_part, ""));
|
|
175
|
-
let patch: u64 = if patch_digits.is_empty() {
|
|
176
|
-
0
|
|
177
|
-
} else {
|
|
178
|
-
patch_digits.parse().ok()?
|
|
179
|
-
};
|
|
180
|
-
// Stability flag: 1 = stable, 0 = pre-release. Stable wins ties.
|
|
181
|
-
let is_stable: u8 = if rest.is_empty() { 1 } else { 0 };
|
|
182
|
-
Some((major, minor, patch, is_stable))
|
|
183
|
-
}
|
|
184
|
-
|
|
185
147
|
/// Return a sort key for a severity string such that Critical sorts first.
|
|
186
148
|
///
|
|
187
149
|
/// Lower numeric values sort earlier, so Critical = 0, Unknown = 5.
|
|
@@ -858,6 +820,43 @@ mod tests {
|
|
|
858
820
|
assert_eq!(fixable.get("pkg").map(|s| s.as_str()), Some("2.10.0"));
|
|
859
821
|
}
|
|
860
822
|
|
|
823
|
+
#[test]
|
|
824
|
+
fn test_compute_fix_plan_picks_numerically_highest_non_semver_fix() {
|
|
825
|
+
// Regression test for 4-segment versions (e.g. Ruby or multi-segment tags)
|
|
826
|
+
// where the legacy `semver_parse` dropped the 4th segment, making
|
|
827
|
+
// `1.0.0.10` and `1.0.0.9` compare equal. `max_by` then returned the
|
|
828
|
+
// *last* element, so the answer depended on fix_version ordering.
|
|
829
|
+
//
|
|
830
|
+
// The correct "minimum safe fix" must order by full numeric value.
|
|
831
|
+
let mut audit = AuditResult::default();
|
|
832
|
+
audit.vulnerable.push(PackageAuditResult {
|
|
833
|
+
package: Package {
|
|
834
|
+
name: "pkg".to_string(),
|
|
835
|
+
version: "1.0.0.1".to_string(),
|
|
836
|
+
ecosystem: Ecosystem::RubyGems,
|
|
837
|
+
},
|
|
838
|
+
vulnerabilities: vec![
|
|
839
|
+
Vulnerability {
|
|
840
|
+
id: "CVE-A".to_string(),
|
|
841
|
+
summary: None,
|
|
842
|
+
severity: None,
|
|
843
|
+
url: None,
|
|
844
|
+
fixed_version: Some("1.0.0.10".to_string()),
|
|
845
|
+
},
|
|
846
|
+
Vulnerability {
|
|
847
|
+
id: "CVE-B".to_string(),
|
|
848
|
+
summary: None,
|
|
849
|
+
severity: None,
|
|
850
|
+
url: None,
|
|
851
|
+
fixed_version: Some("1.0.0.9".to_string()),
|
|
852
|
+
},
|
|
853
|
+
],
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
let (fixable, _) = compute_fix_plan(&audit);
|
|
857
|
+
assert_eq!(fixable.get("pkg").map(String::as_str), Some("1.0.0.10"));
|
|
858
|
+
}
|
|
859
|
+
|
|
861
860
|
// ─── check_packages_cached unit tests ────────────────────────────────────
|
|
862
861
|
|
|
863
862
|
fn sample_package(name: &str) -> Package {
|
|
@@ -130,8 +130,9 @@ pub struct Cli {
|
|
|
130
130
|
|
|
131
131
|
/// Regenerate lockfiles after updating.
|
|
132
132
|
///
|
|
133
|
-
/// Runs the
|
|
134
|
-
/// `
|
|
133
|
+
/// Runs the narrowest per-ecosystem refresh command that updates only the
|
|
134
|
+
/// packages `upd` just rewrote (e.g. `cargo update -p <pkg>`,
|
|
135
|
+
/// `bundle lock --update <pkg>`, `npm install --package-lock-only`).
|
|
135
136
|
#[arg(long, global = true)]
|
|
136
137
|
pub lock: bool,
|
|
137
138
|
|
|
@@ -6,6 +6,7 @@ use anyhow::{Result, anyhow};
|
|
|
6
6
|
use chrono::{DateTime, Duration, Utc};
|
|
7
7
|
|
|
8
8
|
use crate::registry::VersionMeta;
|
|
9
|
+
use crate::version::compare::compare_versions;
|
|
9
10
|
|
|
10
11
|
/// Parse a cooldown duration string.
|
|
11
12
|
///
|
|
@@ -238,46 +239,6 @@ pub fn select(
|
|
|
238
239
|
}
|
|
239
240
|
}
|
|
240
241
|
|
|
241
|
-
/// Version comparison: try semver first, fall back to numeric-aware segment
|
|
242
|
-
/// comparison.
|
|
243
|
-
///
|
|
244
|
-
/// The fallback splits on `.` and `-` and compares integer segments as numbers
|
|
245
|
-
/// (so `1.10 > 1.9` and `v0.10.0.0 > v0.9.0.0`). Lexicographic string compare
|
|
246
|
-
/// would get those wrong, which breaks selection across non-strict-semver
|
|
247
|
-
/// ecosystems like PyPI and multi-segment GitHub tags.
|
|
248
|
-
fn compare_versions(a: &str, b: &str) -> std::cmp::Ordering {
|
|
249
|
-
let stripped_a = a.strip_prefix('v').unwrap_or(a);
|
|
250
|
-
let stripped_b = b.strip_prefix('v').unwrap_or(b);
|
|
251
|
-
if let (Ok(va), Ok(vb)) = (
|
|
252
|
-
semver::Version::parse(stripped_a),
|
|
253
|
-
semver::Version::parse(stripped_b),
|
|
254
|
-
) {
|
|
255
|
-
return va.cmp(&vb);
|
|
256
|
-
}
|
|
257
|
-
compare_loose(stripped_a, stripped_b)
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
fn compare_loose(a: &str, b: &str) -> std::cmp::Ordering {
|
|
261
|
-
let mut a_parts = a.split(['.', '-']);
|
|
262
|
-
let mut b_parts = b.split(['.', '-']);
|
|
263
|
-
loop {
|
|
264
|
-
match (a_parts.next(), b_parts.next()) {
|
|
265
|
-
(None, None) => return std::cmp::Ordering::Equal,
|
|
266
|
-
(None, Some(_)) => return std::cmp::Ordering::Less,
|
|
267
|
-
(Some(_), None) => return std::cmp::Ordering::Greater,
|
|
268
|
-
(Some(x), Some(y)) => {
|
|
269
|
-
let ord = match (x.parse::<u64>(), y.parse::<u64>()) {
|
|
270
|
-
(Ok(nx), Ok(ny)) => nx.cmp(&ny),
|
|
271
|
-
_ => x.cmp(y),
|
|
272
|
-
};
|
|
273
|
-
if ord != std::cmp::Ordering::Equal {
|
|
274
|
-
return ord;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
242
|
fn is_newer(candidate: &str, current: &str) -> bool {
|
|
282
243
|
compare_versions(candidate, current) == std::cmp::Ordering::Greater
|
|
283
244
|
}
|
|
@@ -50,30 +50,20 @@ impl RegenOutcome {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
/// Lockfile
|
|
53
|
+
/// Lockfile variants supported by `upd --lock`. See `command()` for the
|
|
54
|
+
/// concrete invocation used per variant.
|
|
54
55
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
55
56
|
pub enum LockfileType {
|
|
56
|
-
/// poetry.lock - regenerated with `poetry lock --no-update`
|
|
57
57
|
PoetryLock,
|
|
58
|
-
/// uv.lock - regenerated with `uv lock`
|
|
59
58
|
UvLock,
|
|
60
|
-
/// package-lock.json - regenerated with `npm install`
|
|
61
59
|
PackageLockJson,
|
|
62
|
-
/// yarn.lock - regenerated with `yarn install`
|
|
63
60
|
YarnLock,
|
|
64
|
-
/// pnpm-lock.yaml - regenerated with `pnpm install`
|
|
65
61
|
PnpmLock,
|
|
66
|
-
/// bun.lockb - regenerated with `bun install`
|
|
67
62
|
BunLock,
|
|
68
|
-
/// Cargo.lock - regenerated with `cargo update`
|
|
69
63
|
CargoLock,
|
|
70
|
-
/// go.sum - regenerated with `go mod tidy`
|
|
71
64
|
GoSum,
|
|
72
|
-
/// Gemfile.lock - regenerated with `bundle install`
|
|
73
65
|
GemfileLock,
|
|
74
|
-
/// packages.lock.json - regenerated with `dotnet restore`
|
|
75
66
|
PackagesLockJson,
|
|
76
|
-
/// .terraform.lock.hcl - regenerated with `terraform providers lock`
|
|
77
67
|
TerraformLock,
|
|
78
68
|
}
|
|
79
69
|
|
|
@@ -95,20 +85,71 @@ impl LockfileType {
|
|
|
95
85
|
}
|
|
96
86
|
}
|
|
97
87
|
|
|
98
|
-
///
|
|
99
|
-
|
|
88
|
+
/// Returns the command + args to regenerate this lockfile.
|
|
89
|
+
///
|
|
90
|
+
/// `changed` is the list of package names that `upd` just rewrote in the
|
|
91
|
+
/// corresponding manifest. Ecosystems whose CLI supports a targeted form use
|
|
92
|
+
/// it (`cargo update -p …`, `bundle lock --update …`). Ecosystems whose CLI
|
|
93
|
+
/// supports a lockfile-only flag prefer that over a full install. Everything
|
|
94
|
+
/// else falls back to the manifest-wide refresh command.
|
|
95
|
+
pub fn command(&self, changed: &[String]) -> (&'static str, Vec<String>) {
|
|
100
96
|
match self {
|
|
101
|
-
LockfileType::PoetryLock => (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
LockfileType::
|
|
106
|
-
LockfileType::
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
97
|
+
LockfileType::PoetryLock => (
|
|
98
|
+
"poetry",
|
|
99
|
+
vec!["lock".to_string(), "--no-update".to_string()],
|
|
100
|
+
),
|
|
101
|
+
LockfileType::UvLock => ("uv", vec!["lock".to_string()]),
|
|
102
|
+
LockfileType::PackageLockJson => (
|
|
103
|
+
"npm",
|
|
104
|
+
vec!["install".to_string(), "--package-lock-only".to_string()],
|
|
105
|
+
),
|
|
106
|
+
// Yarn Berry (v2+) only: `--mode update-lockfile` is the only
|
|
107
|
+
// documented form that refreshes `yarn.lock` without running
|
|
108
|
+
// install scripts. Yarn 1 (Classic) does not accept `--mode` and
|
|
109
|
+
// will error; Yarn 1 reached EOL in 2023 and is unsupported here.
|
|
110
|
+
LockfileType::YarnLock => (
|
|
111
|
+
"yarn",
|
|
112
|
+
vec![
|
|
113
|
+
"install".to_string(),
|
|
114
|
+
"--mode".to_string(),
|
|
115
|
+
"update-lockfile".to_string(),
|
|
116
|
+
],
|
|
117
|
+
),
|
|
118
|
+
LockfileType::PnpmLock => (
|
|
119
|
+
"pnpm",
|
|
120
|
+
vec!["install".to_string(), "--lockfile-only".to_string()],
|
|
121
|
+
),
|
|
122
|
+
LockfileType::BunLock => ("bun", vec!["install".to_string()]),
|
|
123
|
+
LockfileType::CargoLock => {
|
|
124
|
+
if changed.is_empty() {
|
|
125
|
+
(
|
|
126
|
+
"cargo",
|
|
127
|
+
vec!["update".to_string(), "--workspace".to_string()],
|
|
128
|
+
)
|
|
129
|
+
} else {
|
|
130
|
+
let mut args = vec!["update".to_string()];
|
|
131
|
+
for pkg in changed {
|
|
132
|
+
args.push("-p".to_string());
|
|
133
|
+
args.push(pkg.clone());
|
|
134
|
+
}
|
|
135
|
+
("cargo", args)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
LockfileType::GoSum => ("go", vec!["mod".to_string(), "tidy".to_string()]),
|
|
139
|
+
LockfileType::GemfileLock => {
|
|
140
|
+
if changed.is_empty() {
|
|
141
|
+
("bundle", vec!["lock".to_string()])
|
|
142
|
+
} else {
|
|
143
|
+
let mut args = vec!["lock".to_string(), "--update".to_string()];
|
|
144
|
+
args.extend(changed.iter().cloned());
|
|
145
|
+
("bundle", args)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
LockfileType::PackagesLockJson => ("dotnet", vec!["restore".to_string()]),
|
|
149
|
+
LockfileType::TerraformLock => (
|
|
150
|
+
"terraform",
|
|
151
|
+
vec!["providers".to_string(), "lock".to_string()],
|
|
152
|
+
),
|
|
112
153
|
}
|
|
113
154
|
}
|
|
114
155
|
|
|
@@ -249,15 +290,21 @@ pub fn tool_available(tool: &str) -> bool {
|
|
|
249
290
|
|
|
250
291
|
/// Regenerate a single lockfile by running the appropriate package manager.
|
|
251
292
|
///
|
|
293
|
+
/// `changed` is the list of package names that `upd` just rewrote in the
|
|
294
|
+
/// corresponding manifest. This is forwarded to [`LockfileType::command`] so
|
|
295
|
+
/// ecosystems that support targeted commands (e.g. `cargo update -p …`) only
|
|
296
|
+
/// touch the packages that actually changed.
|
|
297
|
+
///
|
|
252
298
|
/// Returns a [`RegenOutcome`] distinguishing success, missing tool, and
|
|
253
299
|
/// command failure.
|
|
254
|
-
pub fn regenerate_lockfile(
|
|
300
|
+
pub(crate) fn regenerate_lockfile(
|
|
255
301
|
manifest_path: &Path,
|
|
256
302
|
lockfile_type: LockfileType,
|
|
303
|
+
changed: &[String],
|
|
257
304
|
verbose: bool,
|
|
258
305
|
) -> RegenOutcome {
|
|
259
306
|
let dir = manifest_path.parent().unwrap_or(Path::new("."));
|
|
260
|
-
let (cmd, args) = lockfile_type.command();
|
|
307
|
+
let (cmd, args) = lockfile_type.command(changed);
|
|
261
308
|
|
|
262
309
|
if !tool_available(cmd) {
|
|
263
310
|
return RegenOutcome::ToolMissing {
|
|
@@ -279,7 +326,7 @@ pub fn regenerate_lockfile(
|
|
|
279
326
|
);
|
|
280
327
|
}
|
|
281
328
|
|
|
282
|
-
let output = match Command::new(cmd).args(args).current_dir(dir).output() {
|
|
329
|
+
let output = match Command::new(cmd).args(&args).current_dir(dir).output() {
|
|
283
330
|
Ok(o) => o,
|
|
284
331
|
Err(e) => {
|
|
285
332
|
return RegenOutcome::Failed {
|
|
@@ -330,10 +377,19 @@ impl LockfileRegenResult {
|
|
|
330
377
|
|
|
331
378
|
/// Regenerate all lockfiles for a manifest, returning a structured result.
|
|
332
379
|
///
|
|
380
|
+
/// `changed` is the list of package names that `upd` just rewrote in the
|
|
381
|
+
/// corresponding manifest. It is forwarded to each [`regenerate_lockfile`]
|
|
382
|
+
/// call so ecosystems that support targeted commands only touch the packages
|
|
383
|
+
/// that actually changed.
|
|
384
|
+
///
|
|
333
385
|
/// If no lockfiles are detected the caller is responsible for emitting the
|
|
334
386
|
/// `note:` skip message; this function sets `no_lockfiles = true` to signal
|
|
335
387
|
/// that.
|
|
336
|
-
pub fn regenerate_lockfiles(
|
|
388
|
+
pub fn regenerate_lockfiles(
|
|
389
|
+
manifest_path: &Path,
|
|
390
|
+
changed: &[String],
|
|
391
|
+
verbose: bool,
|
|
392
|
+
) -> LockfileRegenResult {
|
|
337
393
|
let lockfiles = detect_lockfiles(manifest_path);
|
|
338
394
|
|
|
339
395
|
if lockfiles.is_empty() {
|
|
@@ -345,7 +401,7 @@ pub fn regenerate_lockfiles(manifest_path: &Path, verbose: bool) -> LockfileRege
|
|
|
345
401
|
|
|
346
402
|
let outcomes = lockfiles
|
|
347
403
|
.into_iter()
|
|
348
|
-
.map(|lf| regenerate_lockfile(manifest_path, lf, verbose))
|
|
404
|
+
.map(|lf| regenerate_lockfile(manifest_path, lf, changed, verbose))
|
|
349
405
|
.collect();
|
|
350
406
|
|
|
351
407
|
LockfileRegenResult {
|
|
@@ -377,29 +433,104 @@ mod tests {
|
|
|
377
433
|
|
|
378
434
|
#[test]
|
|
379
435
|
fn test_lockfile_type_command() {
|
|
380
|
-
let (cmd, args) = LockfileType::PoetryLock.command();
|
|
436
|
+
let (cmd, args) = LockfileType::PoetryLock.command(&[]);
|
|
381
437
|
assert_eq!(cmd, "poetry");
|
|
382
438
|
assert_eq!(args, &["lock", "--no-update"]);
|
|
383
439
|
|
|
384
|
-
let (cmd, args) = LockfileType::UvLock.command();
|
|
440
|
+
let (cmd, args) = LockfileType::UvLock.command(&[]);
|
|
385
441
|
assert_eq!(cmd, "uv");
|
|
386
442
|
assert_eq!(args, &["lock"]);
|
|
443
|
+
}
|
|
387
444
|
|
|
388
|
-
|
|
445
|
+
#[test]
|
|
446
|
+
fn test_package_lock_json_uses_package_lock_only_flag() {
|
|
447
|
+
let (cmd, args) = LockfileType::PackageLockJson.command(&["react".to_string()]);
|
|
389
448
|
assert_eq!(cmd, "npm");
|
|
390
|
-
assert_eq!(args,
|
|
449
|
+
assert_eq!(args, vec!["install", "--package-lock-only"]);
|
|
450
|
+
}
|
|
391
451
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
452
|
+
#[test]
|
|
453
|
+
fn test_pnpm_lock_uses_lockfile_only_flag() {
|
|
454
|
+
let (cmd, args) = LockfileType::PnpmLock.command(&["react".to_string()]);
|
|
455
|
+
assert_eq!(cmd, "pnpm");
|
|
456
|
+
assert_eq!(args, vec!["install", "--lockfile-only"]);
|
|
457
|
+
}
|
|
395
458
|
|
|
396
|
-
|
|
459
|
+
#[test]
|
|
460
|
+
fn test_yarn_lock_uses_mode_update_lockfile_flag() {
|
|
461
|
+
// Yarn Berry (2+) supports --mode update-lockfile; it is the only
|
|
462
|
+
// documented flag that refreshes the lockfile without running install
|
|
463
|
+
// scripts.
|
|
464
|
+
let (cmd, args) = LockfileType::YarnLock.command(&["react".to_string()]);
|
|
465
|
+
assert_eq!(cmd, "yarn");
|
|
466
|
+
assert_eq!(args, vec!["install", "--mode", "update-lockfile"]);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
#[test]
|
|
470
|
+
fn test_cargo_lock_passes_each_changed_package_to_update_p() {
|
|
471
|
+
let changed = vec!["serde".to_string(), "tokio".to_string()];
|
|
472
|
+
let (cmd, args) = LockfileType::CargoLock.command(&changed);
|
|
473
|
+
assert_eq!(cmd, "cargo");
|
|
474
|
+
assert_eq!(args, vec!["update", "-p", "serde", "-p", "tokio"]);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
#[test]
|
|
478
|
+
fn test_cargo_lock_with_empty_changed_list_stays_workspace_broad() {
|
|
479
|
+
// Defensive: an empty changed list should never reach command() from the
|
|
480
|
+
// update path, but if it does (e.g. the `upd lock` subcommand) we emit the
|
|
481
|
+
// broad workspace update so nothing silently regresses.
|
|
482
|
+
let (cmd, args) = LockfileType::CargoLock.command(&[]);
|
|
397
483
|
assert_eq!(cmd, "cargo");
|
|
398
|
-
assert_eq!(args,
|
|
484
|
+
assert_eq!(args, vec!["update", "--workspace"]);
|
|
485
|
+
}
|
|
399
486
|
|
|
400
|
-
|
|
487
|
+
#[test]
|
|
488
|
+
fn test_gemfile_lock_uses_bundle_lock_update_with_changed_packages() {
|
|
489
|
+
let changed = vec!["rails".to_string(), "pg".to_string()];
|
|
490
|
+
let (cmd, args) = LockfileType::GemfileLock.command(&changed);
|
|
491
|
+
assert_eq!(cmd, "bundle");
|
|
492
|
+
assert_eq!(args, vec!["lock", "--update", "rails", "pg"]);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
#[test]
|
|
496
|
+
fn test_gemfile_lock_with_empty_changed_list_uses_plain_bundle_lock() {
|
|
497
|
+
// Without targeted packages, `bundle lock --update` would bump every gem;
|
|
498
|
+
// we emit plain `bundle lock` (refreshes against the current Gemfile
|
|
499
|
+
// without bumping anything).
|
|
500
|
+
let (cmd, args) = LockfileType::GemfileLock.command(&[]);
|
|
501
|
+
assert_eq!(cmd, "bundle");
|
|
502
|
+
assert_eq!(args, vec!["lock"]);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
#[test]
|
|
506
|
+
fn test_go_sum_falls_back_to_mod_tidy_regardless_of_changed_list() {
|
|
507
|
+
let (cmd, args) = LockfileType::GoSum.command(&["golang.org/x/net".to_string()]);
|
|
401
508
|
assert_eq!(cmd, "go");
|
|
402
|
-
assert_eq!(args,
|
|
509
|
+
assert_eq!(args, vec!["mod", "tidy"]);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
#[test]
|
|
513
|
+
fn test_packages_lock_json_falls_back_to_dotnet_restore() {
|
|
514
|
+
let (cmd, args) = LockfileType::PackagesLockJson.command(&["Newtonsoft.Json".to_string()]);
|
|
515
|
+
assert_eq!(cmd, "dotnet");
|
|
516
|
+
assert_eq!(args, vec!["restore"]);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
#[test]
|
|
520
|
+
fn test_terraform_lock_falls_back_to_providers_lock() {
|
|
521
|
+
let (cmd, args) = LockfileType::TerraformLock.command(&["hashicorp/aws".to_string()]);
|
|
522
|
+
assert_eq!(cmd, "terraform");
|
|
523
|
+
assert_eq!(args, vec!["providers", "lock"]);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
#[test]
|
|
527
|
+
fn test_bun_lock_uses_bun_install() {
|
|
528
|
+
// Bun does not have a stable lockfile-only mode; plain `install` is the
|
|
529
|
+
// minimum reliable form. Keeping the test pins the decision so changes
|
|
530
|
+
// here are intentional.
|
|
531
|
+
let (cmd, args) = LockfileType::BunLock.command(&["react".to_string()]);
|
|
532
|
+
assert_eq!(cmd, "bun");
|
|
533
|
+
assert_eq!(args, vec!["install"]);
|
|
403
534
|
}
|
|
404
535
|
|
|
405
536
|
#[test]
|
|
@@ -570,13 +701,6 @@ mod tests {
|
|
|
570
701
|
assert_eq!(LockfileType::GemfileLock.filename(), "Gemfile.lock");
|
|
571
702
|
}
|
|
572
703
|
|
|
573
|
-
#[test]
|
|
574
|
-
fn test_lockfile_type_gemfile_command() {
|
|
575
|
-
let (cmd, args) = LockfileType::GemfileLock.command();
|
|
576
|
-
assert_eq!(cmd, "bundle");
|
|
577
|
-
assert_eq!(args, &["install"]);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
704
|
#[test]
|
|
581
705
|
fn test_lockfile_type_gemfile_manifest() {
|
|
582
706
|
assert_eq!(LockfileType::GemfileLock.manifest(), "Gemfile");
|
|
@@ -630,16 +754,6 @@ mod tests {
|
|
|
630
754
|
);
|
|
631
755
|
}
|
|
632
756
|
|
|
633
|
-
#[test]
|
|
634
|
-
fn test_lockfile_type_packages_lock_json_command() {
|
|
635
|
-
// .NET lockfiles are regenerated via `dotnet restore` (which
|
|
636
|
-
// rewrites `packages.lock.json` next to each .csproj when
|
|
637
|
-
// `RestorePackagesWithLockFile` is enabled).
|
|
638
|
-
let (cmd, args) = LockfileType::PackagesLockJson.command();
|
|
639
|
-
assert_eq!(cmd, "dotnet");
|
|
640
|
-
assert_eq!(args, &["restore"]);
|
|
641
|
-
}
|
|
642
|
-
|
|
643
757
|
#[test]
|
|
644
758
|
fn test_detect_lockfiles_packages_lock_json_for_csproj() {
|
|
645
759
|
let dir = tempdir().unwrap();
|
|
@@ -681,17 +795,6 @@ mod tests {
|
|
|
681
795
|
);
|
|
682
796
|
}
|
|
683
797
|
|
|
684
|
-
#[test]
|
|
685
|
-
fn test_lockfile_type_terraform_lock_hcl_command() {
|
|
686
|
-
// Terraform's dependency-lock file is regenerated with
|
|
687
|
-
// `terraform providers lock -platform=...`. We use the bare form
|
|
688
|
-
// which updates the existing lock in-place for all platforms
|
|
689
|
-
// currently pinned.
|
|
690
|
-
let (cmd, args) = LockfileType::TerraformLock.command();
|
|
691
|
-
assert_eq!(cmd, "terraform");
|
|
692
|
-
assert_eq!(args, &["providers", "lock"]);
|
|
693
|
-
}
|
|
694
|
-
|
|
695
798
|
#[test]
|
|
696
799
|
fn test_detect_lockfiles_terraform_lock_hcl_for_tf_file() {
|
|
697
800
|
let dir = tempdir().unwrap();
|
|
@@ -743,7 +846,7 @@ mod tests {
|
|
|
743
846
|
fs::write(&manifest, "{}").unwrap();
|
|
744
847
|
// Deliberately do NOT create package-lock.json
|
|
745
848
|
|
|
746
|
-
let result = regenerate_lockfiles(&manifest, false);
|
|
849
|
+
let result = regenerate_lockfiles(&manifest, &[], false);
|
|
747
850
|
assert!(
|
|
748
851
|
result.no_lockfiles,
|
|
749
852
|
"no_lockfiles should be true when no lockfile exists beside the manifest"
|
|
@@ -762,7 +865,7 @@ mod tests {
|
|
|
762
865
|
fs::write(&manifest, "[package]").unwrap();
|
|
763
866
|
// Deliberately do NOT create Cargo.lock
|
|
764
867
|
|
|
765
|
-
let result = regenerate_lockfiles(&manifest, false);
|
|
868
|
+
let result = regenerate_lockfiles(&manifest, &[], false);
|
|
766
869
|
assert!(
|
|
767
870
|
result.no_lockfiles,
|
|
768
871
|
"no_lockfiles should be true when Cargo.lock is absent"
|