Skip to content

Commit 3f30f8e

Browse files
authored
feat: lockfile change detection with configurable strategy (#34)
* feat: add lockfile change detection with configurable strategy Implement pure-Rust lockfile parsing and change detection for npm, yarn, pnpm, and bun lockfiles. When a lockfile changes, domino now identifies which direct dependencies were affected (including transitive deps resolved via reverse dependency graph) and marks projects that import those dependencies. Three strategies control the depth of analysis via --lockfile-strategy: - none: skip lockfile processing entirely - direct (default): mark projects importing affected deps - full: also trace the complete reference chain via process_changed_symbol Partially addresses #14 Made-with: Cursor * fix: harden lockfile detection with correctness, security, and test improvements - Fix yarn parser to read base package.json from git revision (not cwd) - Fix pnpm parser to collect deps from ALL workspace importers - Return empty LockfileData when base lockfile missing (was parsing "{}") - Store canonical dep name in LockfileChange cause (not raw import specifier) - Add 256MB size guard on lockfile loading and parsing (OOM protection) - Add depth guard on npm v1 recursive dependency parsing - Return N-API error on invalid lockfile_strategy instead of silent fallback - Scope get_file_from_revision to pub(crate) - Rename is_affected_import -> match_affected_dependency returning Option<&str> - Deduplicate AffectedState construction in Full strategy loop - Use FxHashMap/FxHashSet throughout lockfile module - LazyLock for yarn regex, allocation-free import matching - Eliminate duplicate git merge-base subprocess via tuple return - Zero-clone reverse dep graph using &str references - Add 25 new unit tests (128 total): LockfileStrategy round-trip, npm v1, malformed input, detect_package_manager, yarn scoped, bun JSONC comments, orphan resolution, has_lockfile_changed for all PMs - Add module and function documentation throughout lockfile.rs - Update README with lockfile detection docs and strategies Partially addresses #14 Made-with: Cursor * fix: add Yarn Berry support and fix npm v3 duplicate package detection Two bugs in lockfile change detection: 1. Yarn Berry (v2+/v4) lockfiles use a YAML format completely different from Yarn Classic. The parser only handled Classic, so Berry lockfiles silently failed to detect version changes (version: 3.3.1 vs version "3.3.1") and corrupted the dependency graph with non-dep fields (checksum, linkType). Fix: auto-detect Berry via __metadata header and parse with serde_yaml. 2. npm v3 lockfiles can contain multiple instances of the same package at different node_modules paths (e.g. node_modules/ms and node_modules/debug/node_modules/ms). The parser extracted the bare name for all instances, so later HashMap entries overwrote earlier ones. If the last-processed instance had the same version in both base and current, the actual change was invisible. Fix: use the full node_modules path as the HashMap key for npm v3. Downstream functions (diff, reverse dep graph) extract bare names via package_name_from_key(). - Add parse_yarn_berry_packages() with serde_yaml parsing - Add extract_yarn_berry_name() for npm:/workspace:/patch: protocols - Add yaml_value_to_string() for serde_yaml numeric coercion - Refactor parse_yarn_lockfile into Berry vs Classic dispatch - Rename parse_yarn_dep_line -> parse_yarn_classic_dep_line - Extract extract_direct_deps_from_pkg_json() shared helper - Add package_name_from_key() for full-path → name extraction - Update diff_lockfile_packages and build_reverse_dep_graph to use package_name_from_key for format-agnostic name extraction - Add 7 new tests: Berry parsing, scoped packages, multi-specifier, version change detection, name extraction, npm v3 dedup Made-with: Cursor * fix: address PR review comments for lockfile change detection - Fix parse_pnpm_package_key matching wrong @ in peer dep suffixes like "foo@1.0.0(bar@2.0.0)" and "foo@1.0.0_react@16.0.0" by stripping parenthesized suffixes before parsing and using forward find('@') instead of rfind - Fix lockfile edges silently dropped from Cytoscape graph by treating LockfileChange like AssetChange (mark as direct change) - Add #[non_exhaustive] to LockfileStrategy enum for future-proofing - Fix lossy float-to-string in yaml_value_to_string (1.0 rendered as "1") by delegating to serde_yaml serializer for Number values - Fix BFS vs DFS documentation mismatch in resolve_to_direct_deps - Add comment about npm v1 last-write-wins limitation - Add 4 new tests: pnpm peer-dep suffixes (parens, underscore, scoped) and yaml float preservation Made-with: Cursor * fix: address second round of PR review comments Five correctness and robustness improvements: 1. Add MAX_LOCKFILE_BYTES guard to get_file_from_revision — the base revision path previously bypassed the 256MB size check by buffering the full git show output before parse_lockfile could enforce it. Now checks object size via git cat-file -s before materialization. 2. Include optionalDependencies in direct dependency tracking across all four lockfile parsers (npm, pnpm, yarn, bun). Previously only dependencies and devDependencies were collected, so changes to optional direct deps couldn't be mapped back to importers. 3. Fix pnpm and Yarn parsers using bare package names as HashMap keys, causing last-write-wins when multiple resolved versions exist. Now uses the original lockfile key (e.g. /foo@1.0.0, image-lib@npm:^2.6.0) to preserve all instances. package_name_from_key() extracts bare names for downstream diffing and import matching. 4. Yarn workspaces: collect package.json from root + all workspace packages (via workspace globs) instead of only the root manifest. This ensures workspace-local dependencies are tracked in direct_dependencies. Works for both current and base revisions. 5. Resolve removed deps against the union of current and previous data. Previously only current_data was used for the reverse graph and direct deps, so removed packages couldn't map back to importers. Now merges both revisions' reverse graphs and direct dep sets. Made-with: Cursor * perf: optimize lockfile module performance, security, and test coverage Performance: - match_affected_dependency O(n)→O(1) via hash lookup instead of linear scan over all affected deps per import - Collapse 3 git process spawns per file (cat-file -e + cat-file -s + show) into 1 (cat-file -p) with Option<String> return type - Pre-allocate serde_yaml lookup keys outside hot loops in pnpm and Yarn Berry parsers - Replace from_utf8_lossy().to_string() double-allocation with String::from_utf8() ownership transfer - Add capacity hints (reserve) to FxHashMap in npm, pnpm, bun parsers and reverse dep graph builder - Filter git ls-tree with pathspec to avoid reading entire repo tree - Use FxHashSet for reverse dep graph values (dedup + faster lookup) - Remove extract_npm_package_name wrapper that allocated String just to check is_empty() - Return &str from extract_yarn_berry_name instead of owned String - Avoid Vec allocation in parse_yarn_classic_dep_line via iterator - Move git.rs regexes to LazyLock statics - Build merged direct deps with capacity hint and iterator chain Security: - get_file_from_revision now returns Result<Option<String>> to distinguish missing files from real failures (size limit, git errors) - Add MAX_PKG_JSON_BYTES (10MB) size guard on package.json disk reads - Base workspace enumeration uses git ls-tree instead of filesystem globs, correctly discovering workspaces deleted in the PR Tests (+20 new, 159 total): - package_name_from_key: 8 tests (npm v3, scoped, pnpm, yarn, bare) - glob_match: 4 tests (star, globstar, nested, no-match) - resolve_to_direct_deps: 2 cyclic graph tests - Yarn Classic optionalDependencies parsing - extract_workspace_patterns: 4 tests (array, object, missing, invalid) Addresses latest review comments on PR #34. Made-with: Cursor * fix: harden git operations and error propagation in lockfile module - Restore pre-buffer size guard: use git cat-file -s to check size before materializing content with cat-file -p, preventing OOM for oversized lockfiles (256MB+) in memory-constrained CI - Distinguish missing files from real git errors: inspect stderr from cat-file -s to only return Ok(None) for missing objects, propagate actual failures (corrupt repos, permission errors) as Err - Fix git ls-tree pathspec: */package.json doesn't work with git pathspec matching, now lists all files and filters for package.json paths in Rust so base-revision workspace discovery works correctly - Propagate errors from collect_base_workspace_pkg_jsons and list_package_jsons_at_revision: return Result<Vec<String>> instead of swallowing errors with empty vecs, callers use ? operator Made-with: Cursor * fix: address PR review comments on lockfile and report modules - LockfileChange badge now uses 'direct' CSS class (was 'imported') - LockfileChange and AssetChange now set has_direct=true so the project card shows 'Direct Change' badge instead of falling through to 'Affected' - npm v3 parser now collects direct deps from all workspace package entries (e.g. packages/app-a), not just the root '' entry - bun parser now collects direct deps from all workspace entries, not just the root '' entry Made-with: Cursor * fix: resolve merge conflicts with main - Fix semantic merge conflict in core.rs: update lockfile integration to use new ProjectIndex API (get_package_names_by_path method and &project_index parameter) instead of removed free function - Fix mangled integration_test.rs: separate interleaved test_shared_source_root_all_projects_affected (from main) from setup_lockfile_test_repo (from this branch) - Remove duplicate struct fields from merge overlap in lockfile tests - Add lockfile_strategy field to new shared-source-root test Made-with: Cursor * fix: restore corrupted test_lockfile_no_change_zero_impact after merge The merge interleaved Rust code from test_shared_source_root into the raw string literal for proj-c/src/index.ts, causing the test to write nonsensical TypeScript (mixed with git_in calls) and have duplicate add/commit pairs with the wrong message. Restored to original content. Made-with: Cursor
1 parent 366f1ec commit 3f30f8e

10 files changed

Lines changed: 2918 additions & 25 deletions

File tree

README.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ domino is a drop-in replacement for the TypeScript version of [traf](https://git
1717

1818
- **Semantic Change Detection**: Analyzes actual code changes at the AST level, not just file changes
1919
- **Cross-File Reference Tracking**: Follows symbol references across your entire workspace
20+
- **Lockfile Change Detection**: Detects dependency version changes in npm, yarn, pnpm, and bun lockfiles and traces affected projects
2021
- **Fast Oxc Parser**: 3-5x faster than TypeScript's compiler API
2122
- **Workspace Support**: Works with Nx, Turborepo, and generic npm/yarn/pnpm/bun workspaces
2223
- **Module Resolution**: Uses oxc_resolver (same as Rolldown and Nova) for accurate module resolution
@@ -112,14 +113,46 @@ domino affected --report report.html
112113
- `--report <PATH>`: Generate a detailed analysis report
113114
- `--debug`: Enable debug logging
114115
- `--cwd <PATH>`: Set the current working directory
116+
- `--lockfile-strategy <STRATEGY>`: Lockfile change detection strategy (default: `direct`)
117+
118+
### Lockfile Change Detection
119+
120+
domino automatically detects when your lockfile changes and identifies which projects are affected by dependency version updates. This works with all major package managers:
121+
122+
| Package Manager | Lockfile |
123+
| --------------- | ------------------- |
124+
| npm | `package-lock.json` |
125+
| yarn | `yarn.lock` |
126+
| pnpm | `pnpm-lock.yaml` |
127+
| bun | `bun.lock` |
128+
129+
Three strategies are available via `--lockfile-strategy`:
130+
131+
- **`none`** — Ignore lockfile changes entirely
132+
- **`direct`** (default) — Mark projects that directly import an affected dependency
133+
- **`full`** — Like `direct`, but also traces the full reference chain (e.g. if `lib-a` changed and `ProjectA` imports it, `full` follows all re-exports of `lib-a` symbols to find additional affected projects)
134+
135+
The detection is transitive: if a deeply nested dependency changes, domino walks the reverse dependency graph to find which direct dependency was affected, then finds all projects importing that dependency.
136+
137+
```bash
138+
# Default: detect lockfile changes with "direct" strategy
139+
domino affected
140+
141+
# Disable lockfile detection
142+
domino affected --lockfile-strategy none
143+
144+
# Full reference chain tracing
145+
domino affected --lockfile-strategy full
146+
```
115147

116148
## How It Works
117149

118150
1. **Git Diff Analysis**: Detects which files and specific lines have changed
119151
2. **Semantic Parsing**: Parses all TypeScript/JavaScript files using Oxc
120152
3. **Symbol Resolution**: Identifies which symbols (functions, classes, constants) were modified
121153
4. **Reference Finding**: Recursively finds all cross-file references to those symbols
122-
5. **Project Mapping**: Maps affected files to their owning projects
154+
5. **Lockfile Analysis**: Detects dependency version changes and traces affected imports
155+
6. **Project Mapping**: Maps affected files to their owning projects
123156

124157
## Performance
125158

@@ -147,6 +180,7 @@ Thanks to Rust and Oxc, domino is significantly faster than the TypeScript versi
147180
- **Workspace Discovery** (`src/workspace/`): Discovers projects in Nx, Turbo, and generic npm/yarn/pnpm/bun workspaces
148181
- **Semantic Analyzer** (`src/semantic/analyzer.rs`): Uses Oxc to parse and analyze TypeScript/JavaScript
149182
- **Reference Finder** (`src/semantic/reference_finder.rs`): Tracks cross-file symbol references
183+
- **Lockfile Analyzer** (`src/lockfile.rs`): Parses lockfiles, builds reverse dependency graphs, and detects affected packages
150184
- **Core Algorithm** (`src/core.rs`): Orchestrates the affected detection logic
151185

152186
### Key Technologies

src/cli.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::core;
22
use crate::error::Result;
33
use crate::profiler::Profiler;
4-
use crate::types::TrueAffectedConfig;
4+
use crate::types::{LockfileStrategy, TrueAffectedConfig};
55
use crate::workspace;
66
use clap::{Parser, Subcommand};
77
use colored::Colorize;
@@ -57,6 +57,10 @@ enum Commands {
5757
/// Generate HTML dependency graph report
5858
#[arg(long)]
5959
report: Option<PathBuf>,
60+
61+
/// Lockfile change detection strategy: none, direct, full
62+
#[arg(long, default_value = "direct")]
63+
lockfile_strategy: LockfileStrategy,
6064
},
6165
}
6266

@@ -93,6 +97,7 @@ pub fn run() -> Result<()> {
9397
ts_config,
9498
profile,
9599
report,
100+
lockfile_strategy,
96101
} => {
97102
let cwd = cwd.unwrap_or_else(|| std::env::current_dir().unwrap());
98103

@@ -156,6 +161,7 @@ pub fn run() -> Result<()> {
156161
"build".to_string(),
157162
".git".to_string(),
158163
],
164+
lockfile_strategy,
159165
};
160166

161167
// Use the report-generating version if --report is specified

src/core.rs

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use crate::error::Result;
22
use crate::git;
3+
use crate::lockfile;
34
use crate::profiler::Profiler;
45
use crate::semantic::{AssetReferenceFinder, ReferenceFinder, WorkspaceAnalyzer};
56
use crate::types::{
6-
AffectCause, AffectedProjectInfo, AffectedReport, AffectedResult, ChangedFile, Project,
7-
TrueAffectedConfig,
7+
AffectCause, AffectedProjectInfo, AffectedReport, AffectedResult, ChangedFile, LockfileStrategy,
8+
Project, TrueAffectedConfig,
89
};
910
use crate::utils::{self, ProjectIndex};
1011
use rustc_hash::{FxHashMap, FxHashSet};
@@ -45,8 +46,8 @@ fn find_affected_internal(
4546
debug!("Base: {}", config.base);
4647
debug!("Projects: {}", config.projects.len());
4748

48-
// Step 1: Get changed files from git
49-
let changed_files = git::get_changed_files(&config.cwd, &config.base)?;
49+
// Step 1: Get changed files from git (also returns the merge-base SHA)
50+
let (changed_files, merge_base) = git::get_changed_files(&config.cwd, &config.base)?;
5051
debug!("Found {} changed files", changed_files.len());
5152

5253
if changed_files.is_empty() {
@@ -72,9 +73,18 @@ fn find_affected_internal(
7273
let mut affected_packages = FxHashSet::default();
7374
let mut project_causes: FxHashMap<String, Vec<AffectCause>> = FxHashMap::default();
7475

76+
// Step 5: Partition changed files into source and non-source (excluding lockfiles)
77+
let detected_pm = lockfile::detect_package_manager(&config.cwd);
78+
let lockfile_filename = detected_pm.as_ref().map(|pm| lockfile::lockfile_name(pm));
79+
7580
// Step 6: Partition changed files into source and non-source
7681
let (source_files, asset_files): (Vec<&ChangedFile>, Vec<&ChangedFile>) = changed_files
7782
.iter()
83+
.filter(|f| {
84+
lockfile_filename
85+
.as_ref()
86+
.is_none_or(|name| f.file_path.to_str() != Some(*name))
87+
})
7888
.partition(|f| utils::is_source_file(&f.file_path));
7989

8090
debug!(
@@ -359,6 +369,91 @@ fn find_affected_internal(
359369
}
360370
}
361371

372+
// Step 5c: Process lockfile changes
373+
if !matches!(config.lockfile_strategy, LockfileStrategy::None) {
374+
if let Some(ref pm) = detected_pm {
375+
if lockfile::has_lockfile_changed(&changed_files, pm) {
376+
debug!("Lockfile changed, strategy: {:?}", config.lockfile_strategy);
377+
match lockfile::find_affected_dependencies(&config.cwd, &merge_base, pm) {
378+
Ok(affected_deps) if !affected_deps.is_empty() => {
379+
debug!("Found {} affected direct dependencies", affected_deps.len());
380+
381+
let mut lockfile_visited = FxHashSet::default();
382+
383+
for (file_path, file_imports) in &analyzer.imports {
384+
// Collect (import, matched canonical dep name) pairs
385+
let matching_imports: Vec<_> = file_imports
386+
.iter()
387+
.filter_map(|imp| {
388+
lockfile::match_affected_dependency(&imp.from_module, &affected_deps)
389+
.map(|dep| (imp, dep))
390+
})
391+
.collect();
392+
393+
if matching_imports.is_empty() {
394+
continue;
395+
}
396+
397+
let owning_packages = project_index.get_package_names_by_path(file_path);
398+
for pkg in &owning_packages {
399+
affected_packages.insert(pkg.clone());
400+
if generate_report {
401+
for &(_, dep_name) in &matching_imports {
402+
project_causes.entry(pkg.clone()).or_default().push(
403+
AffectCause::LockfileChange {
404+
dependency: dep_name.to_string(),
405+
importing_file: file_path.clone(),
406+
},
407+
);
408+
}
409+
}
410+
}
411+
412+
if matches!(config.lockfile_strategy, LockfileStrategy::Full) {
413+
for &(imp, _) in &matching_imports {
414+
let symbols_to_trace =
415+
match analyzer.find_exported_symbols_using(file_path, &imp.local_name) {
416+
Ok(exports) if !exports.is_empty() => exports,
417+
Ok(_) => vec![imp.local_name.clone()],
418+
Err(e) => {
419+
debug!("Error finding exports using '{}': {}", imp.local_name, e);
420+
continue;
421+
}
422+
};
423+
424+
for sym in symbols_to_trace {
425+
let mut state = AffectedState {
426+
affected_packages: &mut affected_packages,
427+
project_causes: if generate_report {
428+
Some(&mut project_causes)
429+
} else {
430+
None
431+
},
432+
visited: &mut lockfile_visited,
433+
};
434+
if let Err(e) = process_changed_symbol(
435+
&analyzer,
436+
&reference_finder,
437+
file_path,
438+
&sym,
439+
&project_index,
440+
&mut state,
441+
) {
442+
debug!("Error tracing lockfile symbol '{}': {}", sym, e);
443+
}
444+
}
445+
}
446+
}
447+
}
448+
}
449+
Ok(_) => debug!("Lockfile changed but no dependency versions differ"),
450+
Err(e) => debug!("Lockfile analysis failed, skipping: {}", e),
451+
}
452+
}
453+
}
454+
}
455+
456+
// Step 6: Add implicit dependencies
362457
// Step 7: Add implicit dependencies
363458
add_implicit_dependencies(
364459
&config.projects,

src/git.rs

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@ use crate::types::ChangedFile;
33
use regex::Regex;
44
use std::path::Path;
55
use std::process::Command;
6+
use std::sync::LazyLock;
67
use tracing::{debug, warn};
78

9+
static FILE_RE: LazyLock<Regex> =
10+
LazyLock::new(|| Regex::new(r#"(?:["\s]a/)(.*)(?:["\s]b/)"#).expect("file regex is valid"));
11+
static LINE_RE: LazyLock<Regex> =
12+
LazyLock::new(|| Regex::new(r"@@ -.* \+(\d+)(?:,\d+)? @@").expect("line regex is valid"));
13+
814
/// Detect the default branch (tries origin/main, then origin/master)
915
pub fn detect_default_branch(repo_path: &Path) -> String {
1016
// Try origin/main first
@@ -89,35 +95,30 @@ pub fn get_diff(repo_path: &Path, base: &str) -> Result<String> {
8995
)));
9096
}
9197

92-
Ok(String::from_utf8_lossy(&output.stdout).to_string())
98+
Ok(
99+
String::from_utf8(output.stdout)
100+
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned()),
101+
)
93102
}
94103

95-
/// Parse git diff output to extract changed files and line numbers
96-
pub fn get_changed_files(repo_path: &Path, base: &str) -> Result<Vec<ChangedFile>> {
104+
/// Parse git diff output to extract changed files and line numbers.
105+
/// Returns the changed files along with the computed merge-base SHA.
106+
pub fn get_changed_files(repo_path: &Path, base: &str) -> Result<(Vec<ChangedFile>, String)> {
97107
debug!("Getting diff for base: {}", base);
98108

99-
// First, find the merge base between base and HEAD
100-
// This ensures we only see changes from the current branch, not changes
101-
// from the base branch that happened after branching
102109
let merge_base = get_merge_base(repo_path, base, "HEAD")?;
103110
debug!("Merge base: {}", merge_base);
104111

105-
// Then diff the merge base against the working tree (not HEAD)
106-
// This includes both committed and uncommitted changes, matching traf's behavior
107112
let diff = get_diff(repo_path, &merge_base)?;
113+
let files = parse_diff(&diff)?;
108114

109-
parse_diff(&diff)
115+
Ok((files, merge_base))
110116
}
111117

112118
/// Parse git diff output into ChangedFile structs
113119
fn parse_diff(diff: &str) -> Result<Vec<ChangedFile>> {
114-
// Regex to extract file path: matches "a/path/to/file" between quotes or spaces
115-
let file_regex = Regex::new(r#"(?:["\s]a/)(.*)(?:["\s]b/)"#)
116-
.map_err(|e| DominoError::Parse(format!("Invalid file regex: {}", e)))?;
117-
118-
// Regex to extract line numbers: matches "+<line_number>" in diff header
119-
let line_regex = Regex::new(r"@@ -.* \+(\d+)(?:,\d+)? @@")
120-
.map_err(|e| DominoError::Parse(format!("Invalid line regex: {}", e)))?;
120+
let file_regex = &*FILE_RE;
121+
let line_regex = &*LINE_RE;
121122

122123
let changed_files: Vec<ChangedFile> = diff
123124
.split("diff --git")

src/lib.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub mod cli;
44
pub mod core;
55
pub mod error;
66
pub mod git;
7+
pub mod lockfile;
78
pub mod profiler;
89
pub mod report;
910
pub mod semantic;
@@ -66,6 +67,8 @@ mod napi_bindings {
6667
pub include: Option<Vec<String>>,
6768
pub ignored_paths: Option<Vec<String>>,
6869
pub enable_profiling: Option<bool>,
70+
/// Lockfile change detection strategy: "none", "direct", "full" (default: "direct")
71+
pub lockfile_strategy: Option<String>,
6972
}
7073

7174
#[napi(object)]
@@ -81,13 +84,21 @@ mod napi_bindings {
8184

8285
let profiler = Arc::new(Profiler::new(options.enable_profiling.unwrap_or(false)));
8386

87+
let lockfile_strategy = options
88+
.lockfile_strategy
89+
.as_deref()
90+
.map(|s| s.parse::<LockfileStrategy>().map_err(Error::from_reason))
91+
.transpose()?
92+
.unwrap_or_default();
93+
8494
let config = TrueAffectedConfig {
8595
cwd,
8696
base: options.base,
8797
root_ts_config: options.root_ts_config.map(PathBuf::from),
8898
projects,
8999
include: options.include.unwrap_or_default(),
90100
ignored_paths: options.ignored_paths.unwrap_or_default(),
101+
lockfile_strategy,
91102
};
92103

93104
let result =

0 commit comments

Comments
 (0)