upd-cli 0.1.2__tar.gz → 0.1.4__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.
Files changed (76) hide show
  1. {upd_cli-0.1.2 → upd_cli-0.1.4}/CHANGELOG.md +23 -0
  2. {upd_cli-0.1.2 → upd_cli-0.1.4}/Cargo.lock +1 -1
  3. {upd_cli-0.1.2 → upd_cli-0.1.4}/Cargo.toml +1 -1
  4. {upd_cli-0.1.2 → upd_cli-0.1.4}/PKG-INFO +38 -3
  5. {upd_cli-0.1.2 → upd_cli-0.1.4}/README.md +37 -2
  6. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/audit/mod.rs +38 -39
  7. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/cli.rs +35 -2
  8. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/cooldown.rs +1 -40
  9. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/lib.rs +3 -1
  10. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/lockfile.rs +174 -71
  11. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/main.rs +76 -12
  12. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/updater/mod.rs +372 -79
  13. upd_cli-0.1.4/src/updater/npm_range.rs +257 -0
  14. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/updater/package_json.rs +432 -17
  15. upd_cli-0.1.4/src/version/compare.rs +79 -0
  16. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/version/mod.rs +1 -0
  17. upd_cli-0.1.4/tests/discovery_no_ignore.rs +114 -0
  18. {upd_cli-0.1.2 → upd_cli-0.1.4}/.mise.toml +0 -0
  19. {upd_cli-0.1.2 → upd_cli-0.1.4}/.pre-commit-config.yaml +0 -0
  20. {upd_cli-0.1.2 → upd_cli-0.1.4}/.pre-commit-hooks.yaml +0 -0
  21. {upd_cli-0.1.2 → upd_cli-0.1.4}/.rumdl.toml +0 -0
  22. {upd_cli-0.1.2 → upd_cli-0.1.4}/LICENSE +0 -0
  23. {upd_cli-0.1.2 → upd_cli-0.1.4}/Makefile +0 -0
  24. {upd_cli-0.1.2 → upd_cli-0.1.4}/assets/logo-wide.svg +0 -0
  25. {upd_cli-0.1.2 → upd_cli-0.1.4}/assets/logo.svg +0 -0
  26. {upd_cli-0.1.2 → upd_cli-0.1.4}/pyproject.toml +0 -0
  27. {upd_cli-0.1.2 → upd_cli-0.1.4}/python/upd_cli/__init__.py +0 -0
  28. {upd_cli-0.1.2 → upd_cli-0.1.4}/python/upd_cli/__main__.py +0 -0
  29. {upd_cli-0.1.2 → upd_cli-0.1.4}/python/upd_cli/py.typed +0 -0
  30. {upd_cli-0.1.2 → upd_cli-0.1.4}/rust-toolchain.toml +0 -0
  31. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/align.rs +0 -0
  32. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/audit/cache.rs +0 -0
  33. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/audit/cvss.rs +0 -0
  34. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/cache.rs +0 -0
  35. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/config.rs +0 -0
  36. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/interactive.rs +0 -0
  37. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/output.rs +0 -0
  38. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/registry/crates_io.rs +0 -0
  39. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/registry/github_releases.rs +0 -0
  40. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/registry/go_proxy.rs +0 -0
  41. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/registry/mock.rs +0 -0
  42. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/registry/mod.rs +0 -0
  43. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/registry/npm.rs +0 -0
  44. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/registry/nuget.rs +0 -0
  45. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/registry/pypi.rs +0 -0
  46. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/registry/rubygems.rs +0 -0
  47. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/registry/terraform.rs +0 -0
  48. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/registry/utils.rs +0 -0
  49. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/updater/cargo_toml.rs +0 -0
  50. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/updater/csproj.rs +0 -0
  51. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/updater/gemfile.rs +0 -0
  52. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/updater/github_actions.rs +0 -0
  53. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/updater/go_mod.rs +0 -0
  54. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/updater/mise.rs +0 -0
  55. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/updater/pre_commit.rs +0 -0
  56. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/updater/pyproject.rs +0 -0
  57. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/updater/requirements.rs +0 -0
  58. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/updater/terraform.rs +0 -0
  59. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/version/pep440.rs +0 -0
  60. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/version/semver_util.rs +0 -0
  61. {upd_cli-0.1.2 → upd_cli-0.1.4}/src/version/tag.rs +0 -0
  62. {upd_cli-0.1.2 → upd_cli-0.1.4}/tests/audit_offline.rs +0 -0
  63. {upd_cli-0.1.2 → upd_cli-0.1.4}/tests/audit_sarif.rs +0 -0
  64. {upd_cli-0.1.2 → upd_cli-0.1.4}/tests/audit_severity.rs +0 -0
  65. {upd_cli-0.1.2 → upd_cli-0.1.4}/tests/bump_filter.rs +0 -0
  66. {upd_cli-0.1.2 → upd_cli-0.1.4}/tests/cooldown_e2e.rs +0 -0
  67. {upd_cli-0.1.2 → upd_cli-0.1.4}/tests/exit_codes.rs +0 -0
  68. {upd_cli-0.1.2 → upd_cli-0.1.4}/tests/fix_audit.rs +0 -0
  69. {upd_cli-0.1.2 → upd_cli-0.1.4}/tests/format_json.rs +0 -0
  70. {upd_cli-0.1.2 → upd_cli-0.1.4}/tests/help_text.rs +0 -0
  71. {upd_cli-0.1.2 → upd_cli-0.1.4}/tests/interactive_tty.rs +0 -0
  72. {upd_cli-0.1.2 → upd_cli-0.1.4}/tests/invalid_positional.rs +0 -0
  73. {upd_cli-0.1.2 → upd_cli-0.1.4}/tests/no_args_scope.rs +0 -0
  74. {upd_cli-0.1.2 → upd_cli-0.1.4}/tests/output_streams.rs +0 -0
  75. {upd_cli-0.1.2 → upd_cli-0.1.4}/tests/package_filter.rs +0 -0
  76. {upd_cli-0.1.2 → upd_cli-0.1.4}/vership.toml +0 -0
@@ -12,6 +12,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
12
12
 
13
13
 
14
14
 
15
+
16
+
17
+ ## [0.1.4](https://github.com/rvben/upd/compare/v0.1.3...v0.1.4) - 2026-04-28
18
+
19
+ ### Added
20
+
21
+ - **discovery**: respect .gitignore for hidden ecosystem files and add --no-ignore ([bd44269](https://github.com/rvben/upd/commit/bd442692e4e7c5ab96e1163128ac8f274a771f30))
22
+
23
+ ## [0.1.3](https://github.com/rvben/upd/compare/v0.1.2...v0.1.3) - 2026-04-25
24
+
25
+ ### Added
26
+
27
+ - **lock**: scope lockfile regeneration to the packages upd actually changed ([6b6cfa6](https://github.com/rvben/upd/commit/6b6cfa6fe3e8e8d787e78c7afea24883ebe67833))
28
+ - **npm**: classify and rewrite comparator-range specs ([a60979a](https://github.com/rvben/upd/commit/a60979acd7edb7eb6adfac554a590412a6cf8271))
29
+
30
+ ### Fixed
31
+
32
+ - **lock**: include config pins in targeted regenerate and update CLI help ([207fdf9](https://github.com/rvben/upd/commit/207fdf94af95c74865f828f258ec9cfa61d22085))
33
+ - **npm**: preserve upper bound when pinning comparator-range specs ([fb7c863](https://github.com/rvben/upd/commit/fb7c863de9f07201af310ab13a59696d17fcb766))
34
+ - **npm**: apply config policy and cooldown to comparator-range updates ([be095dd](https://github.com/rvben/upd/commit/be095dd30443dd547fb6913dae551cbd530cb019))
35
+ - **npm**: update comparator-range specs via constraint-aware resolution ([4481b8b](https://github.com/rvben/upd/commit/4481b8bad6a02c6f761cf489a547b5546099e871))
36
+ - **audit**: order fix versions numerically, not lexicographically ([069eb1d](https://github.com/rvben/upd/commit/069eb1dc8771640376957339e40ada7b60a82b0a))
37
+
15
38
  ## [0.1.2](https://github.com/rvben/upd/compare/v0.1.1...v0.1.2) - 2026-04-24
16
39
 
17
40
  ### Added
@@ -2024,7 +2024,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
2024
2024
 
2025
2025
  [[package]]
2026
2026
  name = "upd"
2027
- version = "0.1.2"
2027
+ version = "0.1.4"
2028
2028
  dependencies = [
2029
2029
  "anyhow",
2030
2030
  "async-trait",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "upd"
3
- version = "0.1.2"
3
+ version = "0.1.4"
4
4
  edition = "2024"
5
5
  rust-version = "1.95.0"
6
6
  description = "A fast dependency updater for Python, Node.js, Rust, Go, Ruby, Terraform, GitHub Actions, pre-commit, and Mise projects"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: upd-cli
3
- Version: 0.1.2
3
+ Version: 0.1.4
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 version constraints like `>=2.0,<3` and `~> 7.1`
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`
@@ -54,7 +57,11 @@ pipx run --spec upd-cli upd --apply
54
57
  - **Major warnings**: Highlights breaking changes with `(MAJOR)`
55
58
  - **Format-preserving**: Keeps formatting, comments, and structure
56
59
  - **Pre-release aware**: Updates pre-releases to newer pre-releases
57
- - **Gitignore-aware**: Respects `.gitignore` when discovering files
60
+ - **Gitignore-aware**: Honors `.gitignore`, `.git/info/exclude`, and the global
61
+ gitignore — even outside a git repo. Hidden directories are pruned by default;
62
+ `upd` only opens the dotfiles it actually updates (`.github/workflows`,
63
+ `.pre-commit-config.yaml`, `.mise.toml`, `.tool-versions`). Use `--no-ignore`
64
+ to walk every file regardless.
58
65
  - **Version alignment**: Align package versions across multiple files
59
66
  - **Security auditing**: Check dependencies for known vulnerabilities via OSV
60
67
  - **Config file support**: Ignore or pin packages via `.updrc.toml`
@@ -758,6 +765,7 @@ Global flags (accepted on every subcommand):
758
765
  | `--full-precision` | | Output full versions |
759
766
  | `--no-cache` | | Disable version cache |
760
767
  | `--no-color` | | Disable colored output |
768
+ | `--no-ignore` | | Disable `.gitignore` filtering during discovery |
761
769
  | `--lock` | | Regenerate lockfiles after updates |
762
770
  | `--config <FILE>` | `-c` | Use a specific config file |
763
771
  | `--show-config` | | Print effective configuration and exit |
@@ -767,6 +775,33 @@ Global flags (accepted on every subcommand):
767
775
 
768
776
  Subcommands: `update` (default), `align`, `audit`, `clean-cache`, `self-update`.
769
777
 
778
+ #### Commands run by `--lock`
779
+
780
+ `upd --lock` runs the narrowest per-ecosystem refresh command that
781
+ updates only the packages `upd` just rewrote. Targeted forms are used
782
+ wherever the package manager supports them; targeting falls back to
783
+ `--lockfile-only` flags where no per-package form exists; otherwise
784
+ the manifest-wide refresh command is used.
785
+
786
+ | Ecosystem | Lockfile | Command |
787
+ |-----------|--------------------------|------------------------------------------------|
788
+ | Python | `poetry.lock` | `poetry lock --no-update` |
789
+ | Python | `uv.lock` | `uv lock` |
790
+ | Node | `package-lock.json` | `npm install --package-lock-only` |
791
+ | Node | `yarn.lock` | `yarn install --mode update-lockfile` (Yarn 2+)|
792
+ | Node | `pnpm-lock.yaml` | `pnpm install --lockfile-only` |
793
+ | Node | `bun.lockb` | `bun install` |
794
+ | Rust | `Cargo.lock` | `cargo update -p <changed> -p <changed> …` |
795
+ | Go | `go.sum` | `go mod tidy` (no targeted form) |
796
+ | Ruby | `Gemfile.lock` | `bundle lock --update <changed> …` |
797
+ | .NET | `packages.lock.json` | `dotnet restore` (no targeted form) |
798
+ | Terraform | `.terraform.lock.hcl` | `terraform providers lock` (no targeted form) |
799
+
800
+ Manifests whose `upd` pass produced zero changes have their lockfile
801
+ refresh skipped entirely. A directory where only config pins were
802
+ applied is still refreshed, and the changed-package list includes
803
+ those pinned packages so `cargo update -p <pkg>` / `bundle lock --update <pkg>` stay scoped.
804
+
770
805
  Stable `audit`-specific flags:
771
806
 
772
807
  | 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 version constraints like `>=2.0,<3` and `~> 7.1`
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`
@@ -31,7 +34,11 @@ pipx run --spec upd-cli upd --apply
31
34
  - **Major warnings**: Highlights breaking changes with `(MAJOR)`
32
35
  - **Format-preserving**: Keeps formatting, comments, and structure
33
36
  - **Pre-release aware**: Updates pre-releases to newer pre-releases
34
- - **Gitignore-aware**: Respects `.gitignore` when discovering files
37
+ - **Gitignore-aware**: Honors `.gitignore`, `.git/info/exclude`, and the global
38
+ gitignore — even outside a git repo. Hidden directories are pruned by default;
39
+ `upd` only opens the dotfiles it actually updates (`.github/workflows`,
40
+ `.pre-commit-config.yaml`, `.mise.toml`, `.tool-versions`). Use `--no-ignore`
41
+ to walk every file regardless.
35
42
  - **Version alignment**: Align package versions across multiple files
36
43
  - **Security auditing**: Check dependencies for known vulnerabilities via OSV
37
44
  - **Config file support**: Ignore or pin packages via `.updrc.toml`
@@ -735,6 +742,7 @@ Global flags (accepted on every subcommand):
735
742
  | `--full-precision` | | Output full versions |
736
743
  | `--no-cache` | | Disable version cache |
737
744
  | `--no-color` | | Disable colored output |
745
+ | `--no-ignore` | | Disable `.gitignore` filtering during discovery |
738
746
  | `--lock` | | Regenerate lockfiles after updates |
739
747
  | `--config <FILE>` | `-c` | Use a specific config file |
740
748
  | `--show-config` | | Print effective configuration and exit |
@@ -744,6 +752,33 @@ Global flags (accepted on every subcommand):
744
752
 
745
753
  Subcommands: `update` (default), `align`, `audit`, `clean-cache`, `self-update`.
746
754
 
755
+ #### Commands run by `--lock`
756
+
757
+ `upd --lock` runs the narrowest per-ecosystem refresh command that
758
+ updates only the packages `upd` just rewrote. Targeted forms are used
759
+ wherever the package manager supports them; targeting falls back to
760
+ `--lockfile-only` flags where no per-package form exists; otherwise
761
+ the manifest-wide refresh command is used.
762
+
763
+ | Ecosystem | Lockfile | Command |
764
+ |-----------|--------------------------|------------------------------------------------|
765
+ | Python | `poetry.lock` | `poetry lock --no-update` |
766
+ | Python | `uv.lock` | `uv lock` |
767
+ | Node | `package-lock.json` | `npm install --package-lock-only` |
768
+ | Node | `yarn.lock` | `yarn install --mode update-lockfile` (Yarn 2+)|
769
+ | Node | `pnpm-lock.yaml` | `pnpm install --lockfile-only` |
770
+ | Node | `bun.lockb` | `bun install` |
771
+ | Rust | `Cargo.lock` | `cargo update -p <changed> -p <changed> …` |
772
+ | Go | `go.sum` | `go mod tidy` (no targeted form) |
773
+ | Ruby | `Gemfile.lock` | `bundle lock --update <changed> …` |
774
+ | .NET | `packages.lock.json` | `dotnet restore` (no targeted form) |
775
+ | Terraform | `.terraform.lock.hcl` | `terraform providers lock` (no targeted form) |
776
+
777
+ Manifests whose `upd` pass produced zero changes have their lockfile
778
+ refresh skipped entirely. A directory where only config pins were
779
+ applied is still refreshed, and the changed-package list includes
780
+ those pinned packages so `cargo update -p <pkg>` / `bundle lock --update <pkg>` stay scoped.
781
+
747
782
  Stable `audit`-specific flags:
748
783
 
749
784
  | 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| compare_fix_versions(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 appropriate lock command for each ecosystem (e.g. `poetry lock`,
134
- /// `npm install`, `cargo update`).
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
 
@@ -182,6 +183,15 @@ pub struct Cli {
182
183
  value_delimiter = ','
183
184
  )]
184
185
  pub packages: Vec<String>,
186
+
187
+ /// Disable .gitignore filtering and walk every dependency file in the tree.
188
+ ///
189
+ /// By default, `upd` honors `.gitignore`, `.git/info/exclude`, and the
190
+ /// global gitignore when discovering dependency files. Pass `--no-ignore`
191
+ /// to scan files git would ignore. Equivalent to `rg --no-ignore`.
192
+ /// Explicit file paths are always processed regardless of this flag.
193
+ #[arg(long = "no-ignore", global = true)]
194
+ pub no_ignore: bool,
185
195
  }
186
196
 
187
197
  #[derive(Subcommand)]
@@ -841,4 +851,27 @@ mod tests {
841
851
  assert_eq!(cli.min_age.as_deref(), Some("14d"));
842
852
  assert!(matches!(cli.command, Some(Command::Update { .. })));
843
853
  }
854
+
855
+ #[test]
856
+ fn test_cli_no_ignore_default_false() {
857
+ let cli = Cli::try_parse_from(["upd"]).unwrap();
858
+ assert!(!cli.no_ignore);
859
+ }
860
+
861
+ #[test]
862
+ fn test_cli_no_ignore_parses() {
863
+ let cli = Cli::try_parse_from(["upd", "--no-ignore"]).unwrap();
864
+ assert!(cli.no_ignore);
865
+ }
866
+
867
+ #[test]
868
+ fn test_cli_no_ignore_is_global_across_subcommands() {
869
+ let cli = Cli::try_parse_from(["upd", "audit", "--no-ignore"]).unwrap();
870
+ assert!(cli.no_ignore);
871
+ assert!(matches!(cli.command, Some(Command::Audit { .. })));
872
+
873
+ let cli = Cli::try_parse_from(["upd", "align", "--no-ignore"]).unwrap();
874
+ assert!(cli.no_ignore);
875
+ assert!(matches!(cli.command, Some(Command::Align { .. })));
876
+ }
844
877
  }
@@ -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
  }
@@ -25,7 +25,9 @@ pub use registry::{
25
25
  GitHubReleasesRegistry, NpmRegistry, NuGetRegistry, PyPiRegistry, Registry, RubyGemsRegistry,
26
26
  TerraformRegistry, VersionMeta,
27
27
  };
28
- pub use updater::{FileType, Lang, UpdateResult, Updater, discover_files};
28
+ pub use updater::{
29
+ DiscoverOptions, FileType, Lang, UpdateResult, Updater, discover_files, discover_files_with,
30
+ };
29
31
 
30
32
  /// Determine the process exit code given the outcome of a run.
31
33
  ///