upd-cli 0.1.3__tar.gz → 0.1.5__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.3 → upd_cli-0.1.5}/CHANGELOG.md +14 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/Cargo.lock +1 -1
- {upd_cli-0.1.3 → upd_cli-0.1.5}/Cargo.toml +1 -1
- {upd_cli-0.1.3 → upd_cli-0.1.5}/PKG-INFO +7 -2
- {upd_cli-0.1.3 → upd_cli-0.1.5}/README.md +6 -1
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/audit/mod.rs +208 -3
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/cli.rs +32 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/lib.rs +3 -1
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/main.rs +28 -7
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/updater/mod.rs +370 -79
- upd_cli-0.1.5/tests/discovery_no_ignore.rs +114 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/.mise.toml +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/.pre-commit-config.yaml +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/.pre-commit-hooks.yaml +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/.rumdl.toml +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/LICENSE +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/Makefile +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/assets/logo-wide.svg +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/assets/logo.svg +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/pyproject.toml +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/python/upd_cli/__init__.py +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/python/upd_cli/__main__.py +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/python/upd_cli/py.typed +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/rust-toolchain.toml +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/align.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/audit/cache.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/audit/cvss.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/cache.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/config.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/cooldown.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/interactive.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/lockfile.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/output.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/registry/crates_io.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/registry/github_releases.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/registry/go_proxy.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/registry/mock.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/registry/mod.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/registry/npm.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/registry/nuget.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/registry/pypi.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/registry/rubygems.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/registry/terraform.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/registry/utils.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/updater/cargo_toml.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/updater/csproj.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/updater/gemfile.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/updater/github_actions.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/updater/go_mod.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/updater/mise.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/updater/npm_range.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/updater/package_json.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/updater/pre_commit.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/updater/pyproject.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/updater/requirements.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/updater/terraform.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/version/compare.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/version/mod.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/version/pep440.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/version/semver_util.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/src/version/tag.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/tests/audit_offline.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/tests/audit_sarif.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/tests/audit_severity.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/tests/bump_filter.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/tests/cooldown_e2e.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/tests/exit_codes.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/tests/fix_audit.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/tests/format_json.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/tests/help_text.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/tests/interactive_tty.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/tests/invalid_positional.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/tests/no_args_scope.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/tests/output_streams.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/tests/package_filter.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.5}/vership.toml +0 -0
|
@@ -13,6 +13,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## [0.1.5](https://github.com/rvben/upd/compare/v0.1.4...v0.1.5) - 2026-04-28
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **audit**: include package names and HTTP body in OSV error diagnostics ([87b9aa8](https://github.com/rvben/upd/commit/87b9aa8d27545abf449461c3c3d096a661fa377f))
|
|
23
|
+
|
|
24
|
+
## [0.1.4](https://github.com/rvben/upd/compare/v0.1.3...v0.1.4) - 2026-04-28
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **discovery**: respect .gitignore for hidden ecosystem files and add --no-ignore ([bd44269](https://github.com/rvben/upd/commit/bd442692e4e7c5ab96e1163128ac8f274a771f30))
|
|
29
|
+
|
|
16
30
|
## [0.1.3](https://github.com/rvben/upd/compare/v0.1.2...v0.1.3) - 2026-04-25
|
|
17
31
|
|
|
18
32
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: upd-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Environment :: Console
|
|
6
6
|
Classifier: Intended Audience :: Developers
|
|
@@ -57,7 +57,11 @@ pipx run --spec upd-cli upd --apply
|
|
|
57
57
|
- **Major warnings**: Highlights breaking changes with `(MAJOR)`
|
|
58
58
|
- **Format-preserving**: Keeps formatting, comments, and structure
|
|
59
59
|
- **Pre-release aware**: Updates pre-releases to newer pre-releases
|
|
60
|
-
- **Gitignore-aware**:
|
|
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.
|
|
61
65
|
- **Version alignment**: Align package versions across multiple files
|
|
62
66
|
- **Security auditing**: Check dependencies for known vulnerabilities via OSV
|
|
63
67
|
- **Config file support**: Ignore or pin packages via `.updrc.toml`
|
|
@@ -761,6 +765,7 @@ Global flags (accepted on every subcommand):
|
|
|
761
765
|
| `--full-precision` | | Output full versions |
|
|
762
766
|
| `--no-cache` | | Disable version cache |
|
|
763
767
|
| `--no-color` | | Disable colored output |
|
|
768
|
+
| `--no-ignore` | | Disable `.gitignore` filtering during discovery |
|
|
764
769
|
| `--lock` | | Regenerate lockfiles after updates |
|
|
765
770
|
| `--config <FILE>` | `-c` | Use a specific config file |
|
|
766
771
|
| `--show-config` | | Print effective configuration and exit |
|
|
@@ -34,7 +34,11 @@ pipx run --spec upd-cli upd --apply
|
|
|
34
34
|
- **Major warnings**: Highlights breaking changes with `(MAJOR)`
|
|
35
35
|
- **Format-preserving**: Keeps formatting, comments, and structure
|
|
36
36
|
- **Pre-release aware**: Updates pre-releases to newer pre-releases
|
|
37
|
-
- **Gitignore-aware**:
|
|
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.
|
|
38
42
|
- **Version alignment**: Align package versions across multiple files
|
|
39
43
|
- **Security auditing**: Check dependencies for known vulnerabilities via OSV
|
|
40
44
|
- **Config file support**: Ignore or pin packages via `.updrc.toml`
|
|
@@ -738,6 +742,7 @@ Global flags (accepted on every subcommand):
|
|
|
738
742
|
| `--full-precision` | | Output full versions |
|
|
739
743
|
| `--no-cache` | | Disable version cache |
|
|
740
744
|
| `--no-color` | | Disable colored output |
|
|
745
|
+
| `--no-ignore` | | Disable `.gitignore` filtering during discovery |
|
|
741
746
|
| `--lock` | | Regenerate lockfiles after updates |
|
|
742
747
|
| `--config <FILE>` | `-c` | Use a specific config file |
|
|
743
748
|
| `--show-config` | | Print effective configuration and exit |
|
|
@@ -20,6 +20,35 @@ const BATCH_SIZE: usize = 1000;
|
|
|
20
20
|
/// Maximum concurrent requests for fetching vulnerability details
|
|
21
21
|
const MAX_CONCURRENT_REQUESTS: usize = 20;
|
|
22
22
|
|
|
23
|
+
/// Cap for how many bytes of a non-success OSV response body we surface in
|
|
24
|
+
/// an error message. A proxy or CDN can return arbitrarily large HTML error
|
|
25
|
+
/// pages on the failure path, so we bound the read at the network layer
|
|
26
|
+
/// (via `Response::chunk`) rather than buffering the whole body and then
|
|
27
|
+
/// truncating. 1 KiB comfortably fits typical OSV/proxy diagnostics.
|
|
28
|
+
const ERROR_BODY_LIMIT: usize = 1024;
|
|
29
|
+
|
|
30
|
+
/// Read up to `max` bytes from `response`'s body and return them as a
|
|
31
|
+
/// lossy UTF-8 string. Stops early at the first chunk that fills the cap
|
|
32
|
+
/// so an oversized or slow body cannot stall the call up to the client
|
|
33
|
+
/// timeout or balloon memory beyond `max` bytes.
|
|
34
|
+
async fn read_bounded_body(response: &mut reqwest::Response, max: usize) -> String {
|
|
35
|
+
let mut buf: Vec<u8> = Vec::with_capacity(max);
|
|
36
|
+
while buf.len() < max {
|
|
37
|
+
match response.chunk().await {
|
|
38
|
+
Ok(Some(chunk)) => {
|
|
39
|
+
let remaining = max - buf.len();
|
|
40
|
+
let take = chunk.len().min(remaining);
|
|
41
|
+
buf.extend_from_slice(&chunk[..take]);
|
|
42
|
+
if take < chunk.len() {
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
Ok(None) | Err(_) => break,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
String::from_utf8_lossy(&buf).into_owned()
|
|
50
|
+
}
|
|
51
|
+
|
|
23
52
|
/// Ecosystem names for OSV API
|
|
24
53
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
25
54
|
pub enum Ecosystem {
|
|
@@ -273,7 +302,12 @@ impl OsvClient {
|
|
|
273
302
|
}
|
|
274
303
|
}
|
|
275
304
|
Err(e) => {
|
|
276
|
-
|
|
305
|
+
let pkg_names: Vec<&str> = chunk.iter().map(|p| p.name.as_str()).collect();
|
|
306
|
+
result.errors.push(format!(
|
|
307
|
+
"Batch query failed for [{}]: {}",
|
|
308
|
+
pkg_names.join(", "),
|
|
309
|
+
e
|
|
310
|
+
));
|
|
277
311
|
}
|
|
278
312
|
}
|
|
279
313
|
}
|
|
@@ -299,7 +333,7 @@ impl OsvClient {
|
|
|
299
333
|
|
|
300
334
|
let request = OsvBatchRequest { queries };
|
|
301
335
|
|
|
302
|
-
let response = self
|
|
336
|
+
let mut response = self
|
|
303
337
|
.client
|
|
304
338
|
.post(format!("{}/querybatch", self.base_url))
|
|
305
339
|
.json(&request)
|
|
@@ -307,7 +341,13 @@ impl OsvClient {
|
|
|
307
341
|
.await?;
|
|
308
342
|
|
|
309
343
|
if !response.status().is_success() {
|
|
310
|
-
|
|
344
|
+
let status = response.status();
|
|
345
|
+
let snippet = read_bounded_body(&mut response, ERROR_BODY_LIMIT).await;
|
|
346
|
+
if snippet.is_empty() {
|
|
347
|
+
anyhow::bail!("OSV API error: HTTP {}", status);
|
|
348
|
+
} else {
|
|
349
|
+
anyhow::bail!("OSV API error: HTTP {}: {}", status, snippet);
|
|
350
|
+
}
|
|
311
351
|
}
|
|
312
352
|
|
|
313
353
|
let batch_response: OsvBatchResponse = response.json().await?;
|
|
@@ -1003,4 +1043,169 @@ mod tests {
|
|
|
1003
1043
|
assert_eq!(result.safe_count, 1);
|
|
1004
1044
|
// wiremock will assert that no requests were received when the server drops.
|
|
1005
1045
|
}
|
|
1046
|
+
|
|
1047
|
+
// ─── batch failure error message ─────────────────────────────────────────
|
|
1048
|
+
|
|
1049
|
+
#[tokio::test]
|
|
1050
|
+
async fn batch_failure_error_includes_package_names_status_and_body() {
|
|
1051
|
+
use wiremock::matchers::{method, path};
|
|
1052
|
+
use wiremock::{Mock, MockServer, ResponseTemplate};
|
|
1053
|
+
|
|
1054
|
+
let server = MockServer::start().await;
|
|
1055
|
+
|
|
1056
|
+
// OSV returns a non-success response with a body. Both should surface
|
|
1057
|
+
// in the AuditResult error so operators can diagnose the failure.
|
|
1058
|
+
Mock::given(method("POST"))
|
|
1059
|
+
.and(path("/querybatch"))
|
|
1060
|
+
.respond_with(
|
|
1061
|
+
ResponseTemplate::new(503).set_body_string("upstream temporarily unavailable"),
|
|
1062
|
+
)
|
|
1063
|
+
.mount(&server)
|
|
1064
|
+
.await;
|
|
1065
|
+
|
|
1066
|
+
let client = OsvClient::with_base_url(server.uri());
|
|
1067
|
+
let pkgs = vec![
|
|
1068
|
+
Package {
|
|
1069
|
+
name: "requests".into(),
|
|
1070
|
+
version: "2.0.0".into(),
|
|
1071
|
+
ecosystem: Ecosystem::PyPI,
|
|
1072
|
+
},
|
|
1073
|
+
Package {
|
|
1074
|
+
name: "boto3".into(),
|
|
1075
|
+
version: "1.0.0".into(),
|
|
1076
|
+
ecosystem: Ecosystem::PyPI,
|
|
1077
|
+
},
|
|
1078
|
+
];
|
|
1079
|
+
|
|
1080
|
+
let result = client.check_packages(&pkgs).await.unwrap();
|
|
1081
|
+
|
|
1082
|
+
assert!(result.vulnerable.is_empty());
|
|
1083
|
+
assert_eq!(
|
|
1084
|
+
result.errors.len(),
|
|
1085
|
+
1,
|
|
1086
|
+
"one error for the failing batch, got {:?}",
|
|
1087
|
+
result.errors
|
|
1088
|
+
);
|
|
1089
|
+
let err = &result.errors[0];
|
|
1090
|
+
assert!(
|
|
1091
|
+
err.contains("requests") && err.contains("boto3"),
|
|
1092
|
+
"error should list the failing batch's package names: {err}"
|
|
1093
|
+
);
|
|
1094
|
+
assert!(
|
|
1095
|
+
err.contains("503"),
|
|
1096
|
+
"error should include the HTTP status: {err}"
|
|
1097
|
+
);
|
|
1098
|
+
assert!(
|
|
1099
|
+
err.contains("upstream temporarily unavailable"),
|
|
1100
|
+
"error should include the response body: {err}"
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
#[tokio::test]
|
|
1105
|
+
async fn batch_failure_stops_reading_at_cap_when_body_stalls() {
|
|
1106
|
+
// Stand up a TCP server that lies about Content-Length: it claims
|
|
1107
|
+
// 1 MB but only delivers 2 KiB, then stalls forever. The bounded
|
|
1108
|
+
// read in query_batch must fill its 1 KiB cap from the head of
|
|
1109
|
+
// the body and return immediately. An unbounded read (e.g.
|
|
1110
|
+
// `response.text().await`) would block until either the rest of
|
|
1111
|
+
// the body arrives or the 30 s client timeout fires.
|
|
1112
|
+
use std::time::{Duration, Instant};
|
|
1113
|
+
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
1114
|
+
use tokio::net::TcpListener;
|
|
1115
|
+
|
|
1116
|
+
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
1117
|
+
let addr = listener.local_addr().unwrap();
|
|
1118
|
+
|
|
1119
|
+
let server = tokio::spawn(async move {
|
|
1120
|
+
let (mut stream, _) = listener.accept().await.unwrap();
|
|
1121
|
+
|
|
1122
|
+
// Drain the request until we see the empty header line.
|
|
1123
|
+
let mut buf = [0u8; 4096];
|
|
1124
|
+
loop {
|
|
1125
|
+
let n = match stream.read(&mut buf).await {
|
|
1126
|
+
Ok(n) if n > 0 => n,
|
|
1127
|
+
_ => return,
|
|
1128
|
+
};
|
|
1129
|
+
if buf[..n].windows(4).any(|w| w == b"\r\n\r\n") {
|
|
1130
|
+
break;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// Headers promise 1 MB; we send only 2 KiB then stall.
|
|
1135
|
+
let head = b"HTTP/1.1 503 Service Unavailable\r\n\
|
|
1136
|
+
Content-Type: text/plain\r\n\
|
|
1137
|
+
Content-Length: 1000000\r\n\
|
|
1138
|
+
Connection: close\r\n\r\n";
|
|
1139
|
+
let _ = stream.write_all(head).await;
|
|
1140
|
+
let _ = stream.write_all(&vec![b'x'; 2048]).await;
|
|
1141
|
+
let _ = stream.flush().await;
|
|
1142
|
+
|
|
1143
|
+
// Park the task so the connection stays open without further
|
|
1144
|
+
// bytes. The test will abort the handle after the client
|
|
1145
|
+
// returns.
|
|
1146
|
+
std::future::pending::<()>().await;
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
let client = OsvClient::with_base_url(format!("http://{}", addr));
|
|
1150
|
+
let started = Instant::now();
|
|
1151
|
+
let outcome = tokio::time::timeout(
|
|
1152
|
+
Duration::from_secs(10),
|
|
1153
|
+
client.check_packages(&[Package {
|
|
1154
|
+
name: "pkg".into(),
|
|
1155
|
+
version: "1.0.0".into(),
|
|
1156
|
+
ecosystem: Ecosystem::PyPI,
|
|
1157
|
+
}]),
|
|
1158
|
+
)
|
|
1159
|
+
.await;
|
|
1160
|
+
let elapsed = started.elapsed();
|
|
1161
|
+
|
|
1162
|
+
server.abort();
|
|
1163
|
+
|
|
1164
|
+
let result = outcome
|
|
1165
|
+
.expect(
|
|
1166
|
+
"OSV call must return promptly when the body stalls; \
|
|
1167
|
+
if it didn't, the body is being read unbounded",
|
|
1168
|
+
)
|
|
1169
|
+
.unwrap();
|
|
1170
|
+
|
|
1171
|
+
assert_eq!(result.errors.len(), 1, "{:?}", result.errors);
|
|
1172
|
+
let err = &result.errors[0];
|
|
1173
|
+
assert!(err.contains("503"), "should include status: {err}");
|
|
1174
|
+
// 5 s leaves a >100x margin over the expected ~1 RTT return
|
|
1175
|
+
// time, while staying well under the 30 s client timeout that
|
|
1176
|
+
// the buggy implementation would hit.
|
|
1177
|
+
assert!(
|
|
1178
|
+
elapsed < Duration::from_secs(5),
|
|
1179
|
+
"bounded read should return promptly; took {:?}",
|
|
1180
|
+
elapsed
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
#[tokio::test]
|
|
1185
|
+
async fn batch_failure_error_includes_package_names_on_network_error() {
|
|
1186
|
+
// Point at a closed port so reqwest fails to connect. Package names
|
|
1187
|
+
// should still be embedded in the resulting error message.
|
|
1188
|
+
let client = OsvClient::with_base_url("http://127.0.0.1:1/".to_string());
|
|
1189
|
+
let pkgs = vec![
|
|
1190
|
+
Package {
|
|
1191
|
+
name: "lonely-pkg".into(),
|
|
1192
|
+
version: "1.0.0".into(),
|
|
1193
|
+
ecosystem: Ecosystem::PyPI,
|
|
1194
|
+
},
|
|
1195
|
+
Package {
|
|
1196
|
+
name: "another-pkg".into(),
|
|
1197
|
+
version: "2.0.0".into(),
|
|
1198
|
+
ecosystem: Ecosystem::PyPI,
|
|
1199
|
+
},
|
|
1200
|
+
];
|
|
1201
|
+
|
|
1202
|
+
let result = client.check_packages(&pkgs).await.unwrap();
|
|
1203
|
+
|
|
1204
|
+
assert_eq!(result.errors.len(), 1, "{:?}", result.errors);
|
|
1205
|
+
let err = &result.errors[0];
|
|
1206
|
+
assert!(
|
|
1207
|
+
err.contains("lonely-pkg") && err.contains("another-pkg"),
|
|
1208
|
+
"network-level errors should still name the failing packages: {err}"
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1006
1211
|
}
|
|
@@ -183,6 +183,15 @@ pub struct Cli {
|
|
|
183
183
|
value_delimiter = ','
|
|
184
184
|
)]
|
|
185
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,
|
|
186
195
|
}
|
|
187
196
|
|
|
188
197
|
#[derive(Subcommand)]
|
|
@@ -842,4 +851,27 @@ mod tests {
|
|
|
842
851
|
assert_eq!(cli.min_age.as_deref(), Some("14d"));
|
|
843
852
|
assert!(matches!(cli.command, Some(Command::Update { .. })));
|
|
844
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
|
+
}
|
|
845
877
|
}
|
|
@@ -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::{
|
|
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
|
///
|
|
@@ -22,10 +22,10 @@ use upd::registry::{
|
|
|
22
22
|
NuGetRegistry, PyPiRegistry, RubyGemsRegistry, TerraformRegistry,
|
|
23
23
|
};
|
|
24
24
|
use upd::updater::{
|
|
25
|
-
CargoTomlUpdater, CsprojUpdater, FileType, GemfileUpdater,
|
|
26
|
-
Lang, MiseUpdater, PackageJsonUpdater, PreCommitUpdater,
|
|
27
|
-
TerraformUpdater, UpdateOptions, UpdateResult, Updater,
|
|
28
|
-
write_file_atomic,
|
|
25
|
+
CargoTomlUpdater, CsprojUpdater, DiscoverOptions, FileType, GemfileUpdater,
|
|
26
|
+
GithubActionsUpdater, GoModUpdater, Lang, MiseUpdater, PackageJsonUpdater, PreCommitUpdater,
|
|
27
|
+
PyProjectUpdater, RequirementsUpdater, TerraformUpdater, UpdateOptions, UpdateResult, Updater,
|
|
28
|
+
discover_files_with, read_file_safe, write_file_atomic,
|
|
29
29
|
};
|
|
30
30
|
use upd::version::match_version_precision;
|
|
31
31
|
|
|
@@ -546,7 +546,14 @@ async fn run_update(cli: &Cli) -> Result<()> {
|
|
|
546
546
|
// or --dry-run), the run behaves as dry-run and tells the user how to apply.
|
|
547
547
|
let effective_dry_run = cli.is_effective_dry_run();
|
|
548
548
|
|
|
549
|
-
let files =
|
|
549
|
+
let files = discover_files_with(
|
|
550
|
+
&paths,
|
|
551
|
+
&cli.langs,
|
|
552
|
+
DiscoverOptions {
|
|
553
|
+
no_ignore: cli.no_ignore,
|
|
554
|
+
verbose: cli.verbose,
|
|
555
|
+
},
|
|
556
|
+
);
|
|
550
557
|
let file_count = files.len();
|
|
551
558
|
|
|
552
559
|
let text_mode_early = cli.format == upd::cli::OutputFormat::Text;
|
|
@@ -1493,7 +1500,14 @@ async fn run_align(cli: &Cli) -> Result<()> {
|
|
|
1493
1500
|
}
|
|
1494
1501
|
};
|
|
1495
1502
|
|
|
1496
|
-
let files =
|
|
1503
|
+
let files = discover_files_with(
|
|
1504
|
+
&paths,
|
|
1505
|
+
&cli.langs,
|
|
1506
|
+
DiscoverOptions {
|
|
1507
|
+
no_ignore: cli.no_ignore,
|
|
1508
|
+
verbose: cli.verbose,
|
|
1509
|
+
},
|
|
1510
|
+
);
|
|
1497
1511
|
let file_count = files.len();
|
|
1498
1512
|
|
|
1499
1513
|
if files.is_empty() {
|
|
@@ -1701,7 +1715,14 @@ async fn run_audit(cli: &Cli) -> Result<()> {
|
|
|
1701
1715
|
explicit
|
|
1702
1716
|
}
|
|
1703
1717
|
};
|
|
1704
|
-
let files =
|
|
1718
|
+
let files = discover_files_with(
|
|
1719
|
+
&paths,
|
|
1720
|
+
&cli.langs,
|
|
1721
|
+
DiscoverOptions {
|
|
1722
|
+
no_ignore: cli.no_ignore,
|
|
1723
|
+
verbose: cli.verbose,
|
|
1724
|
+
},
|
|
1725
|
+
);
|
|
1705
1726
|
let file_count = files.len();
|
|
1706
1727
|
|
|
1707
1728
|
if files.is_empty() {
|