upd-cli 0.1.3__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.
- {upd_cli-0.1.3 → upd_cli-0.1.4}/CHANGELOG.md +7 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/Cargo.lock +1 -1
- {upd_cli-0.1.3 → upd_cli-0.1.4}/Cargo.toml +1 -1
- {upd_cli-0.1.3 → upd_cli-0.1.4}/PKG-INFO +7 -2
- {upd_cli-0.1.3 → upd_cli-0.1.4}/README.md +6 -1
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/cli.rs +32 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/lib.rs +3 -1
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/main.rs +28 -7
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/updater/mod.rs +370 -79
- upd_cli-0.1.4/tests/discovery_no_ignore.rs +114 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/.mise.toml +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/.pre-commit-config.yaml +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/.pre-commit-hooks.yaml +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/.rumdl.toml +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/LICENSE +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/Makefile +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/assets/logo-wide.svg +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/assets/logo.svg +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/pyproject.toml +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/python/upd_cli/__init__.py +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/python/upd_cli/__main__.py +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/python/upd_cli/py.typed +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/rust-toolchain.toml +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/align.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/audit/cache.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/audit/cvss.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/audit/mod.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/cache.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/config.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/cooldown.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/interactive.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/lockfile.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/output.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/registry/crates_io.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/registry/github_releases.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/registry/go_proxy.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/registry/mock.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/registry/mod.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/registry/npm.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/registry/nuget.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/registry/pypi.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/registry/rubygems.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/registry/terraform.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/registry/utils.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/updater/cargo_toml.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/updater/csproj.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/updater/gemfile.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/updater/github_actions.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/updater/go_mod.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/updater/mise.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/updater/npm_range.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/updater/package_json.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/updater/pre_commit.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/updater/pyproject.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/updater/requirements.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/updater/terraform.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/version/compare.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/version/mod.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/version/pep440.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/version/semver_util.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/src/version/tag.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/tests/audit_offline.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/tests/audit_sarif.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/tests/audit_severity.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/tests/bump_filter.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/tests/cooldown_e2e.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/tests/exit_codes.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/tests/fix_audit.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/tests/format_json.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/tests/help_text.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/tests/interactive_tty.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/tests/invalid_positional.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/tests/no_args_scope.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/tests/output_streams.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/tests/package_filter.rs +0 -0
- {upd_cli-0.1.3 → upd_cli-0.1.4}/vership.toml +0 -0
|
@@ -13,6 +13,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
13
13
|
|
|
14
14
|
|
|
15
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
|
+
|
|
16
23
|
## [0.1.3](https://github.com/rvben/upd/compare/v0.1.2...v0.1.3) - 2026-04-25
|
|
17
24
|
|
|
18
25
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: upd-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
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 |
|
|
@@ -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() {
|
|
@@ -384,6 +384,19 @@ impl FileType {
|
|
|
384
384
|
return Some(FileType::ToolVersions);
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
// GitHub Actions workflows: *.yml or *.yaml inside .github/workflows/
|
|
388
|
+
if (file_name.ends_with(".yml") || file_name.ends_with(".yaml"))
|
|
389
|
+
&& let Some(parent) = path.parent()
|
|
390
|
+
&& parent.file_name().and_then(|n| n.to_str()) == Some("workflows")
|
|
391
|
+
&& parent
|
|
392
|
+
.parent()
|
|
393
|
+
.and_then(|gp| gp.file_name())
|
|
394
|
+
.and_then(|n| n.to_str())
|
|
395
|
+
== Some(".github")
|
|
396
|
+
{
|
|
397
|
+
return Some(FileType::GithubActions);
|
|
398
|
+
}
|
|
399
|
+
|
|
387
400
|
// Terraform .tf files (exclude files inside .terraform/ directories)
|
|
388
401
|
if file_name.ends_with(".tf") {
|
|
389
402
|
let path_str = path.to_string_lossy();
|
|
@@ -538,102 +551,126 @@ pub async fn apply_cooldown(
|
|
|
538
551
|
}
|
|
539
552
|
}
|
|
540
553
|
|
|
541
|
-
///
|
|
542
|
-
///
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
///
|
|
563
|
-
|
|
564
|
-
fn discover_pre_commit_config(path: &Path, files: &mut Vec<(PathBuf, FileType)>) {
|
|
565
|
-
let config_path = path.join(".pre-commit-config.yaml");
|
|
566
|
-
if config_path.is_file() {
|
|
567
|
-
files.push((config_path, FileType::PreCommitConfig));
|
|
568
|
-
}
|
|
554
|
+
/// Hidden entries the walker is allowed to descend into or yield.
|
|
555
|
+
///
|
|
556
|
+
/// Anything not in this set that starts with `.` is pruned during discovery,
|
|
557
|
+
/// which keeps `.git`, `.cache`, `.venv`, and similar noise out of the scan
|
|
558
|
+
/// while still letting us see the dotfiles `upd` actually updates.
|
|
559
|
+
const ALLOWED_HIDDEN_ENTRIES: &[&str] = &[
|
|
560
|
+
".github",
|
|
561
|
+
".pre-commit-config.yaml",
|
|
562
|
+
".mise.toml",
|
|
563
|
+
".tool-versions",
|
|
564
|
+
];
|
|
565
|
+
|
|
566
|
+
/// Knobs for [`discover_files_with`].
|
|
567
|
+
#[derive(Debug, Clone, Copy, Default)]
|
|
568
|
+
pub struct DiscoverOptions {
|
|
569
|
+
/// When true, ignore `.gitignore`, `.git/info/exclude`, and the global
|
|
570
|
+
/// gitignore — walk every dependency file regardless. Mirrors
|
|
571
|
+
/// `rg --no-ignore`.
|
|
572
|
+
pub no_ignore: bool,
|
|
573
|
+
/// When true, emit one `skipping <path>: gitignored` line on stderr for
|
|
574
|
+
/// each dependency file the gitignore rules dropped, so users can see
|
|
575
|
+
/// why `upd` is silent on a given file.
|
|
576
|
+
pub verbose: bool,
|
|
569
577
|
}
|
|
570
578
|
|
|
571
|
-
///
|
|
572
|
-
///
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
files.push((tool_versions_path, FileType::ToolVersions));
|
|
582
|
-
}
|
|
579
|
+
/// Discover dependency files in the given paths, optionally filtered by language.
|
|
580
|
+
///
|
|
581
|
+
/// Directory walks honor `.gitignore`, `.git/info/exclude`, and the global
|
|
582
|
+
/// gitignore — even outside a git repository. Hidden directories and files are
|
|
583
|
+
/// skipped except for the small allowlist in [`ALLOWED_HIDDEN_ENTRIES`]. Explicit
|
|
584
|
+
/// file paths bypass the filter and are always processed.
|
|
585
|
+
///
|
|
586
|
+
/// Convenience wrapper around [`discover_files_with`] with default options.
|
|
587
|
+
pub fn discover_files(paths: &[PathBuf], langs: &[Lang]) -> Vec<(PathBuf, FileType)> {
|
|
588
|
+
discover_files_with(paths, langs, DiscoverOptions::default())
|
|
583
589
|
}
|
|
584
590
|
|
|
585
|
-
|
|
586
|
-
|
|
591
|
+
/// Discover dependency files with explicit [`DiscoverOptions`].
|
|
592
|
+
pub fn discover_files_with(
|
|
593
|
+
paths: &[PathBuf],
|
|
587
594
|
langs: &[Lang],
|
|
588
|
-
|
|
589
|
-
) {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
595
|
+
options: DiscoverOptions,
|
|
596
|
+
) -> Vec<(PathBuf, FileType)> {
|
|
597
|
+
let kept = walk_dependency_files(paths, langs, options.no_ignore);
|
|
598
|
+
|
|
599
|
+
// In verbose mode, surface the dependency files gitignore rules dropped so
|
|
600
|
+
// users can answer "why didn't upd touch X?" without guessing. Skipped when
|
|
601
|
+
// gitignore is already disabled (nothing to diff against).
|
|
602
|
+
if options.verbose && !options.no_ignore {
|
|
603
|
+
let unrestricted = walk_dependency_files(paths, langs, true);
|
|
604
|
+
let kept_set: std::collections::HashSet<&Path> =
|
|
605
|
+
kept.iter().map(|(p, _)| p.as_path()).collect();
|
|
606
|
+
for (path, _) in &unrestricted {
|
|
607
|
+
if !kept_set.contains(path.as_path()) {
|
|
608
|
+
eprintln!("skipping {}: gitignored", path.display());
|
|
609
|
+
}
|
|
610
|
+
}
|
|
596
611
|
}
|
|
597
612
|
|
|
598
|
-
|
|
599
|
-
discover_mise_files(path, files);
|
|
600
|
-
}
|
|
613
|
+
kept
|
|
601
614
|
}
|
|
602
615
|
|
|
603
|
-
|
|
604
|
-
|
|
616
|
+
fn walk_dependency_files(
|
|
617
|
+
paths: &[PathBuf],
|
|
618
|
+
langs: &[Lang],
|
|
619
|
+
no_ignore: bool,
|
|
620
|
+
) -> Vec<(PathBuf, FileType)> {
|
|
605
621
|
let mut files = Vec::new();
|
|
606
622
|
|
|
607
623
|
for path in paths {
|
|
608
|
-
if path.is_file()
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
624
|
+
if path.is_file() {
|
|
625
|
+
if let Some(file_type) = FileType::detect(path)
|
|
626
|
+
&& (langs.is_empty() || langs.contains(&file_type.lang()))
|
|
627
|
+
{
|
|
612
628
|
files.push((path.clone(), file_type));
|
|
613
629
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if !path.is_dir() {
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
let walker = WalkBuilder::new(path)
|
|
638
|
+
.hidden(false)
|
|
639
|
+
.git_ignore(!no_ignore)
|
|
640
|
+
.git_global(!no_ignore)
|
|
641
|
+
.git_exclude(!no_ignore)
|
|
642
|
+
.require_git(false)
|
|
643
|
+
.filter_entry(|entry| {
|
|
644
|
+
// Always traverse the user-supplied root, even when it is hidden
|
|
645
|
+
// (e.g. `upd .github/workflows`).
|
|
646
|
+
if entry.depth() == 0 {
|
|
647
|
+
return true;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
let name = entry.file_name().to_string_lossy();
|
|
651
|
+
|
|
652
|
+
// `.git` is internal — never descend into it.
|
|
653
|
+
if name == ".git" {
|
|
654
|
+
return false;
|
|
629
655
|
}
|
|
630
656
|
|
|
631
|
-
if
|
|
632
|
-
|
|
633
|
-
&& (langs.is_empty() || langs.contains(&file_type.lang()))
|
|
634
|
-
{
|
|
635
|
-
files.push((entry_path.to_path_buf(), file_type));
|
|
657
|
+
if !name.starts_with('.') {
|
|
658
|
+
return true;
|
|
636
659
|
}
|
|
660
|
+
|
|
661
|
+
ALLOWED_HIDDEN_ENTRIES.contains(&name.as_ref())
|
|
662
|
+
})
|
|
663
|
+
.build();
|
|
664
|
+
|
|
665
|
+
for entry in walker.flatten() {
|
|
666
|
+
let entry_path = entry.path();
|
|
667
|
+
if !entry_path.is_file() {
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
if let Some(file_type) = FileType::detect(entry_path)
|
|
671
|
+
&& (langs.is_empty() || langs.contains(&file_type.lang()))
|
|
672
|
+
{
|
|
673
|
+
files.push((entry_path.to_path_buf(), file_type));
|
|
637
674
|
}
|
|
638
675
|
}
|
|
639
676
|
}
|
|
@@ -1118,6 +1155,260 @@ mod tests {
|
|
|
1118
1155
|
let files = discover_files(&[temp.path().to_path_buf()], &[Lang::Actions]);
|
|
1119
1156
|
assert!(files.is_empty());
|
|
1120
1157
|
}
|
|
1158
|
+
|
|
1159
|
+
/// Detection of a workflow file should depend on the .github/workflows
|
|
1160
|
+
/// path components, not just the extension. A bare YAML file elsewhere
|
|
1161
|
+
/// must not be classified as a GitHub Actions workflow.
|
|
1162
|
+
#[test]
|
|
1163
|
+
fn test_filetype_detect_github_actions_requires_workflows_dir() {
|
|
1164
|
+
assert_eq!(
|
|
1165
|
+
FileType::detect(Path::new("/repo/.github/workflows/ci.yml")),
|
|
1166
|
+
Some(FileType::GithubActions)
|
|
1167
|
+
);
|
|
1168
|
+
assert_eq!(
|
|
1169
|
+
FileType::detect(Path::new("/repo/.github/workflows/release.yaml")),
|
|
1170
|
+
Some(FileType::GithubActions)
|
|
1171
|
+
);
|
|
1172
|
+
// A workflow nested under a sub-project still counts.
|
|
1173
|
+
assert_eq!(
|
|
1174
|
+
FileType::detect(Path::new("/repo/apps/api/.github/workflows/ci.yml")),
|
|
1175
|
+
Some(FileType::GithubActions)
|
|
1176
|
+
);
|
|
1177
|
+
// Anything outside .github/workflows is not a workflow.
|
|
1178
|
+
assert_eq!(
|
|
1179
|
+
FileType::detect(Path::new("/repo/.github/dependabot.yml")),
|
|
1180
|
+
None
|
|
1181
|
+
);
|
|
1182
|
+
assert_eq!(FileType::detect(Path::new("/repo/random.yml")), None);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/// Files listed in .gitignore must not be discovered when walking a
|
|
1186
|
+
/// directory. This is the single most-requested default — users expect
|
|
1187
|
+
/// `upd` to ignore the same things git does.
|
|
1188
|
+
#[test]
|
|
1189
|
+
fn test_discover_files_respects_gitignore() {
|
|
1190
|
+
let temp = tempdir().unwrap();
|
|
1191
|
+
let root = temp.path();
|
|
1192
|
+
|
|
1193
|
+
fs::write(
|
|
1194
|
+
root.join(".gitignore"),
|
|
1195
|
+
"ignored.txt\nvendor/\n.github/workflows/internal.yml\n.pre-commit-config.yaml\n.mise.toml\n.tool-versions\n",
|
|
1196
|
+
)
|
|
1197
|
+
.unwrap();
|
|
1198
|
+
|
|
1199
|
+
// Regular files: kept.
|
|
1200
|
+
fs::write(root.join("requirements.txt"), "flask>=2.0").unwrap();
|
|
1201
|
+
// Regular file: ignored by name.
|
|
1202
|
+
fs::write(root.join("ignored.txt"), "should-not-appear").unwrap();
|
|
1203
|
+
// Whole gitignored directory.
|
|
1204
|
+
fs::create_dir_all(root.join("vendor")).unwrap();
|
|
1205
|
+
fs::write(
|
|
1206
|
+
root.join("vendor").join("Cargo.toml"),
|
|
1207
|
+
"[package]\nname=\"x\"",
|
|
1208
|
+
)
|
|
1209
|
+
.unwrap();
|
|
1210
|
+
|
|
1211
|
+
// GitHub Actions: one ignored, one kept.
|
|
1212
|
+
let workflows = root.join(".github").join("workflows");
|
|
1213
|
+
fs::create_dir_all(&workflows).unwrap();
|
|
1214
|
+
fs::write(workflows.join("ci.yml"), "name: CI").unwrap();
|
|
1215
|
+
fs::write(workflows.join("internal.yml"), "name: Internal").unwrap();
|
|
1216
|
+
|
|
1217
|
+
// Hidden ecosystem files: gitignored.
|
|
1218
|
+
fs::write(root.join(".pre-commit-config.yaml"), "repos: []").unwrap();
|
|
1219
|
+
fs::write(root.join(".mise.toml"), "[tools]").unwrap();
|
|
1220
|
+
fs::write(root.join(".tool-versions"), "node 20").unwrap();
|
|
1221
|
+
|
|
1222
|
+
let files = discover_files(&[root.to_path_buf()], &[]);
|
|
1223
|
+
let paths: Vec<PathBuf> = files.iter().map(|(p, _)| p.clone()).collect();
|
|
1224
|
+
|
|
1225
|
+
assert!(paths.contains(&root.join("requirements.txt")));
|
|
1226
|
+
assert!(paths.contains(&workflows.join("ci.yml")));
|
|
1227
|
+
|
|
1228
|
+
for forbidden in [
|
|
1229
|
+
root.join("ignored.txt"),
|
|
1230
|
+
root.join("vendor").join("Cargo.toml"),
|
|
1231
|
+
workflows.join("internal.yml"),
|
|
1232
|
+
root.join(".pre-commit-config.yaml"),
|
|
1233
|
+
root.join(".mise.toml"),
|
|
1234
|
+
root.join(".tool-versions"),
|
|
1235
|
+
] {
|
|
1236
|
+
assert!(
|
|
1237
|
+
!paths.contains(&forbidden),
|
|
1238
|
+
"discover_files should skip gitignored {forbidden:?}; got {paths:#?}"
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/// Negation patterns (`!foo`) must un-ignore matching paths so users
|
|
1244
|
+
/// can ship a top-level ignore but selectively keep individual files.
|
|
1245
|
+
#[test]
|
|
1246
|
+
fn test_discover_files_respects_gitignore_negation() {
|
|
1247
|
+
let temp = tempdir().unwrap();
|
|
1248
|
+
let root = temp.path();
|
|
1249
|
+
|
|
1250
|
+
fs::write(
|
|
1251
|
+
root.join(".gitignore"),
|
|
1252
|
+
".github/workflows/*.yml\n!.github/workflows/keep.yml\n",
|
|
1253
|
+
)
|
|
1254
|
+
.unwrap();
|
|
1255
|
+
|
|
1256
|
+
let workflows = root.join(".github").join("workflows");
|
|
1257
|
+
fs::create_dir_all(&workflows).unwrap();
|
|
1258
|
+
fs::write(workflows.join("drop.yml"), "name: Drop").unwrap();
|
|
1259
|
+
fs::write(workflows.join("keep.yml"), "name: Keep").unwrap();
|
|
1260
|
+
|
|
1261
|
+
let files = discover_files(&[root.to_path_buf()], &[Lang::Actions]);
|
|
1262
|
+
let paths: Vec<PathBuf> = files.iter().map(|(p, _)| p.clone()).collect();
|
|
1263
|
+
|
|
1264
|
+
assert!(paths.contains(&workflows.join("keep.yml")));
|
|
1265
|
+
assert!(!paths.contains(&workflows.join("drop.yml")));
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/// Explicit file arguments bypass the gitignore filter — `upd
|
|
1269
|
+
/// path/to/file` should always process the file the user pointed at,
|
|
1270
|
+
/// matching `rg` / `fd` semantics for explicit paths.
|
|
1271
|
+
#[test]
|
|
1272
|
+
fn test_discover_files_explicit_path_bypasses_gitignore() {
|
|
1273
|
+
let temp = tempdir().unwrap();
|
|
1274
|
+
let root = temp.path();
|
|
1275
|
+
|
|
1276
|
+
fs::write(root.join(".gitignore"), ".mise.toml\n").unwrap();
|
|
1277
|
+
let mise = root.join(".mise.toml");
|
|
1278
|
+
fs::write(&mise, "[tools]").unwrap();
|
|
1279
|
+
|
|
1280
|
+
// Directory walk: gitignored, so excluded.
|
|
1281
|
+
let walked = discover_files(&[root.to_path_buf()], &[]);
|
|
1282
|
+
assert!(walked.iter().all(|(p, _)| p != &mise));
|
|
1283
|
+
|
|
1284
|
+
// Explicit path: included regardless of gitignore.
|
|
1285
|
+
let direct = discover_files(std::slice::from_ref(&mise), &[]);
|
|
1286
|
+
assert_eq!(direct, vec![(mise, FileType::MiseToml)]);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/// `--no-ignore` (via `DiscoverOptions::no_ignore`) must bypass
|
|
1290
|
+
/// gitignore entirely, including for hidden ecosystem files.
|
|
1291
|
+
#[test]
|
|
1292
|
+
fn test_discover_files_with_no_ignore_bypasses_gitignore() {
|
|
1293
|
+
let temp = tempdir().unwrap();
|
|
1294
|
+
let root = temp.path();
|
|
1295
|
+
|
|
1296
|
+
fs::write(
|
|
1297
|
+
root.join(".gitignore"),
|
|
1298
|
+
"ignored.txt\n.mise.toml\n.tool-versions\n",
|
|
1299
|
+
)
|
|
1300
|
+
.unwrap();
|
|
1301
|
+
fs::write(root.join("ignored.txt"), "x").unwrap();
|
|
1302
|
+
fs::write(root.join("requirements.txt"), "flask>=2.0").unwrap();
|
|
1303
|
+
fs::write(root.join(".mise.toml"), "[tools]").unwrap();
|
|
1304
|
+
fs::write(root.join(".tool-versions"), "node 20").unwrap();
|
|
1305
|
+
|
|
1306
|
+
let unrestricted = discover_files_with(
|
|
1307
|
+
&[root.to_path_buf()],
|
|
1308
|
+
&[],
|
|
1309
|
+
DiscoverOptions {
|
|
1310
|
+
no_ignore: true,
|
|
1311
|
+
verbose: false,
|
|
1312
|
+
},
|
|
1313
|
+
);
|
|
1314
|
+
let paths: Vec<PathBuf> = unrestricted.iter().map(|(p, _)| p.clone()).collect();
|
|
1315
|
+
|
|
1316
|
+
assert!(paths.contains(&root.join("requirements.txt")));
|
|
1317
|
+
assert!(paths.contains(&root.join(".mise.toml")));
|
|
1318
|
+
assert!(paths.contains(&root.join(".tool-versions")));
|
|
1319
|
+
// ignored.txt is not a dependency file, so it never appears regardless;
|
|
1320
|
+
// the meaningful assertion is that the hidden ecosystem files are picked
|
|
1321
|
+
// up despite being gitignored.
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
/// `--lang` filtering must compose with gitignore filtering: ignored
|
|
1325
|
+
/// files of the requested language stay out, kept files of other
|
|
1326
|
+
/// languages also stay out.
|
|
1327
|
+
#[test]
|
|
1328
|
+
fn test_discover_files_lang_filter_composes_with_gitignore() {
|
|
1329
|
+
let temp = tempdir().unwrap();
|
|
1330
|
+
let root = temp.path();
|
|
1331
|
+
|
|
1332
|
+
fs::write(root.join(".gitignore"), "requirements.txt\n").unwrap();
|
|
1333
|
+
fs::write(root.join("requirements.txt"), "flask").unwrap();
|
|
1334
|
+
fs::write(root.join("pyproject.toml"), "[project]\nname='x'").unwrap();
|
|
1335
|
+
fs::write(root.join("package.json"), "{}").unwrap();
|
|
1336
|
+
|
|
1337
|
+
let files = discover_files(&[root.to_path_buf()], &[Lang::Python]);
|
|
1338
|
+
let paths: Vec<PathBuf> = files.iter().map(|(p, _)| p.clone()).collect();
|
|
1339
|
+
|
|
1340
|
+
// gitignored Python file is dropped, non-Python file is dropped by --lang
|
|
1341
|
+
assert_eq!(paths, vec![root.join("pyproject.toml")]);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/// Nested `.gitignore` files (in a subdirectory) must be honored — the
|
|
1345
|
+
/// `ignore` crate handles this; this test pins the behavior so a future
|
|
1346
|
+
/// refactor can't regress it.
|
|
1347
|
+
#[test]
|
|
1348
|
+
fn test_discover_files_respects_nested_gitignore() {
|
|
1349
|
+
let temp = tempdir().unwrap();
|
|
1350
|
+
let root = temp.path();
|
|
1351
|
+
|
|
1352
|
+
// Top-level allows everything.
|
|
1353
|
+
fs::write(root.join(".gitignore"), "").unwrap();
|
|
1354
|
+
// Nested .gitignore drops a single file in its directory.
|
|
1355
|
+
let sub = root.join("sub");
|
|
1356
|
+
fs::create_dir_all(&sub).unwrap();
|
|
1357
|
+
fs::write(sub.join(".gitignore"), "secret.toml\n").unwrap();
|
|
1358
|
+
fs::write(sub.join("secret.toml"), "").unwrap();
|
|
1359
|
+
fs::write(sub.join("Cargo.toml"), "[package]\nname='x'").unwrap();
|
|
1360
|
+
fs::write(sub.join("package.json"), "{}").unwrap();
|
|
1361
|
+
|
|
1362
|
+
let files = discover_files(&[root.to_path_buf()], &[]);
|
|
1363
|
+
let paths: Vec<PathBuf> = files.iter().map(|(p, _)| p.clone()).collect();
|
|
1364
|
+
|
|
1365
|
+
assert!(paths.contains(&sub.join("Cargo.toml")));
|
|
1366
|
+
assert!(paths.contains(&sub.join("package.json")));
|
|
1367
|
+
assert!(!paths.contains(&sub.join("secret.toml")));
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/// A whole-directory ignore (`.github/`) must prune the entire subtree,
|
|
1371
|
+
/// not just a single workflow file.
|
|
1372
|
+
#[test]
|
|
1373
|
+
fn test_discover_files_directory_ignore_prunes_subtree() {
|
|
1374
|
+
let temp = tempdir().unwrap();
|
|
1375
|
+
let root = temp.path();
|
|
1376
|
+
|
|
1377
|
+
fs::write(root.join(".gitignore"), ".github/\n").unwrap();
|
|
1378
|
+
let workflows = root.join(".github").join("workflows");
|
|
1379
|
+
fs::create_dir_all(&workflows).unwrap();
|
|
1380
|
+
fs::write(workflows.join("ci.yml"), "name: CI").unwrap();
|
|
1381
|
+
fs::write(workflows.join("release.yaml"), "name: Release").unwrap();
|
|
1382
|
+
fs::write(root.join("package.json"), "{}").unwrap();
|
|
1383
|
+
|
|
1384
|
+
let files = discover_files(&[root.to_path_buf()], &[]);
|
|
1385
|
+
let paths: Vec<PathBuf> = files.iter().map(|(p, _)| p.clone()).collect();
|
|
1386
|
+
|
|
1387
|
+
assert_eq!(paths, vec![root.join("package.json")]);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
/// `.git` directories must never be walked into — they are not in
|
|
1391
|
+
/// `.gitignore` (git itself manages them) but still contain YAML/TOML
|
|
1392
|
+
/// content that would otherwise be picked up.
|
|
1393
|
+
#[test]
|
|
1394
|
+
fn test_discover_files_skips_dot_git_directory() {
|
|
1395
|
+
let temp = tempdir().unwrap();
|
|
1396
|
+
let root = temp.path();
|
|
1397
|
+
|
|
1398
|
+
let inner_git = root.join(".git").join("workflows-cache");
|
|
1399
|
+
fs::create_dir_all(&inner_git).unwrap();
|
|
1400
|
+
// A file matching one of our patterns, planted inside .git/.
|
|
1401
|
+
fs::write(inner_git.join(".mise.toml"), "[tools]").unwrap();
|
|
1402
|
+
fs::write(root.join(".git").join("config"), "[core]").unwrap();
|
|
1403
|
+
|
|
1404
|
+
let files = discover_files(&[root.to_path_buf()], &[]);
|
|
1405
|
+
for (path, _) in &files {
|
|
1406
|
+
assert!(
|
|
1407
|
+
!path.to_string_lossy().contains("/.git/"),
|
|
1408
|
+
"discover_files must not descend into .git/, found {path:?}"
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1121
1412
|
}
|
|
1122
1413
|
|
|
1123
1414
|
#[cfg(test)]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
//! Integration tests for `.gitignore`-aware discovery and the
|
|
2
|
+
//! `--no-ignore` escape hatch.
|
|
3
|
+
//!
|
|
4
|
+
//! These run the real binary against a temp workspace so the full path
|
|
5
|
+
//! (CLI parsing → DiscoverOptions → walker → output) is covered.
|
|
6
|
+
|
|
7
|
+
use std::fs;
|
|
8
|
+
use std::path::Path;
|
|
9
|
+
use std::process::Command;
|
|
10
|
+
|
|
11
|
+
fn upd_bin() -> &'static str {
|
|
12
|
+
env!("CARGO_BIN_EXE_upd")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
fn run(args: &[&str], cwd: &Path) -> (String, String, i32) {
|
|
16
|
+
let output = Command::new(upd_bin())
|
|
17
|
+
.args(args)
|
|
18
|
+
.env("UPD_CACHE_DIR", cwd.join("upd-cache"))
|
|
19
|
+
.output()
|
|
20
|
+
.expect("failed to run upd");
|
|
21
|
+
(
|
|
22
|
+
String::from_utf8_lossy(&output.stdout).into_owned(),
|
|
23
|
+
String::from_utf8_lossy(&output.stderr).into_owned(),
|
|
24
|
+
output.status.code().unwrap_or(-1),
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Build a workspace where one of two `package.json` files is gitignored.
|
|
29
|
+
/// Returns the temp dir, kept-file path, and ignored-file path.
|
|
30
|
+
fn workspace_with_gitignored_package_json()
|
|
31
|
+
-> (tempfile::TempDir, std::path::PathBuf, std::path::PathBuf) {
|
|
32
|
+
let tmp = tempfile::tempdir().unwrap();
|
|
33
|
+
let root = tmp.path();
|
|
34
|
+
|
|
35
|
+
fs::write(root.join(".gitignore"), "vendor/\n").unwrap();
|
|
36
|
+
|
|
37
|
+
let kept = root.join("package.json");
|
|
38
|
+
fs::write(&kept, "{\"dependencies\":{}}").unwrap();
|
|
39
|
+
|
|
40
|
+
let vendor = root.join("vendor");
|
|
41
|
+
fs::create_dir_all(&vendor).unwrap();
|
|
42
|
+
let ignored = vendor.join("package.json");
|
|
43
|
+
fs::write(&ignored, "{\"dependencies\":{}}").unwrap();
|
|
44
|
+
|
|
45
|
+
(tmp, kept, ignored)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Default behavior must skip the gitignored file: only one file is scanned.
|
|
49
|
+
#[test]
|
|
50
|
+
fn discovery_skips_gitignored_files_by_default() {
|
|
51
|
+
let (tmp, _kept, _ignored) = workspace_with_gitignored_package_json();
|
|
52
|
+
let path = tmp.path().to_str().unwrap();
|
|
53
|
+
|
|
54
|
+
let (stdout, stderr, _code) = run(&["--check", "--no-cache", path], tmp.path());
|
|
55
|
+
|
|
56
|
+
let combined = format!("{stdout}\n{stderr}");
|
|
57
|
+
assert!(
|
|
58
|
+
combined.contains("Scanned 1 file"),
|
|
59
|
+
"default discovery must skip the gitignored file (expect 1 scanned); got:\n{combined}"
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// `--no-ignore` must include the gitignored file: two files scanned.
|
|
64
|
+
#[test]
|
|
65
|
+
fn discovery_no_ignore_flag_includes_gitignored_files() {
|
|
66
|
+
let (tmp, _kept, _ignored) = workspace_with_gitignored_package_json();
|
|
67
|
+
let path = tmp.path().to_str().unwrap();
|
|
68
|
+
|
|
69
|
+
let (stdout, stderr, _code) = run(&["--check", "--no-cache", "--no-ignore", path], tmp.path());
|
|
70
|
+
|
|
71
|
+
let combined = format!("{stdout}\n{stderr}");
|
|
72
|
+
assert!(
|
|
73
|
+
combined.contains("Scanned 2 file"),
|
|
74
|
+
"with --no-ignore both files must be scanned; got:\n{combined}"
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// `--verbose` must surface a `skipping ... gitignored` line on stderr so
|
|
79
|
+
/// users can see why a file they expected to be processed is being silent.
|
|
80
|
+
#[test]
|
|
81
|
+
fn discovery_verbose_logs_gitignored_skip() {
|
|
82
|
+
let (tmp, _kept, ignored) = workspace_with_gitignored_package_json();
|
|
83
|
+
let path = tmp.path().to_str().unwrap();
|
|
84
|
+
|
|
85
|
+
let (_stdout, stderr, _code) = run(&["--check", "--no-cache", "--verbose", path], tmp.path());
|
|
86
|
+
|
|
87
|
+
let ignored_str = ignored.to_str().unwrap();
|
|
88
|
+
assert!(
|
|
89
|
+
stderr.contains("skipping") && stderr.contains("gitignored"),
|
|
90
|
+
"verbose mode must emit a 'skipping ... gitignored' line; stderr:\n{stderr}"
|
|
91
|
+
);
|
|
92
|
+
assert!(
|
|
93
|
+
stderr.contains(ignored_str) || stderr.contains("vendor/package.json"),
|
|
94
|
+
"verbose skip log must mention the ignored path; stderr:\n{stderr}"
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// `--no-ignore` with `--verbose` should NOT emit skip lines (nothing was
|
|
99
|
+
/// skipped — gitignore is disabled). Pinning this avoids spurious noise.
|
|
100
|
+
#[test]
|
|
101
|
+
fn discovery_no_ignore_with_verbose_emits_no_skip_lines() {
|
|
102
|
+
let (tmp, _kept, _ignored) = workspace_with_gitignored_package_json();
|
|
103
|
+
let path = tmp.path().to_str().unwrap();
|
|
104
|
+
|
|
105
|
+
let (_stdout, stderr, _code) = run(
|
|
106
|
+
&["--check", "--no-cache", "--no-ignore", "--verbose", path],
|
|
107
|
+
tmp.path(),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
assert!(
|
|
111
|
+
!stderr.contains("gitignored"),
|
|
112
|
+
"with --no-ignore there is nothing to skip; stderr should not mention gitignored:\n{stderr}"
|
|
113
|
+
);
|
|
114
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|