From f49abcc1ac3de72bf894ccfc0102d83ec19f1d46 Mon Sep 17 00:00:00 2001 From: Divy Srivastava Date: Fri, 23 Feb 2024 07:56:34 +0530 Subject: [PATCH] feat(publish): respect .gitignore during `deno publish` (#22514) Files from `.gitignore`, global git config, `.git/info/exclude` and `deno.json`'s `exclude` are ignored. --- Cargo.lock | 51 +++++++++++++++++++++++ cli/Cargo.toml | 1 + cli/tools/registry/tar.rs | 56 +++++++++++++++++-------- tests/integration/publish_tests.rs | 67 +++++++++++++++++++++++++++++- 4 files changed, 156 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1cb8c9d48d..5e6d1ee4e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -499,6 +499,16 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bstr" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.14.0" @@ -1060,6 +1070,7 @@ dependencies = [ "glibc_version", "glob", "hex", + "ignore", "import_map", "indexmap", "jsonc-parser", @@ -2847,6 +2858,19 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "globset" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + [[package]] name = "glow" version = "0.13.0" @@ -3314,6 +3338,23 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" +[[package]] +name = "ignore" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" +dependencies = [ + "globset", + "lazy_static", + "log", + "memchr", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + [[package]] name = "image" version = "0.24.7" @@ -6482,6 +6523,16 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.30" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index ae42d3c301..b376f924eb 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -106,6 +106,7 @@ flate2.workspace = true fs3.workspace = true glob = "0.3.1" hex.workspace = true +ignore = "0.4" import_map = { version = "=0.18.3", features = ["ext"] } indexmap.workspace = true jsonc-parser = { version = "=0.23.0", features = ["serde"] } diff --git a/cli/tools/registry/tar.rs b/cli/tools/registry/tar.rs index 3dc2616fa0..66d15b5a65 100644 --- a/cli/tools/registry/tar.rs +++ b/cli/tools/registry/tar.rs @@ -3,12 +3,14 @@ use bytes::Bytes; use deno_ast::MediaType; use deno_config::glob::FilePatterns; +use deno_config::glob::PathOrPattern; use deno_core::anyhow::Context; use deno_core::error::AnyError; use deno_core::url::Url; +use ignore::overrides::OverrideBuilder; +use ignore::WalkBuilder; use sha2::Digest; use std::collections::HashSet; -use std::ffi::OsStr; use std::fmt::Write as FmtWrite; use std::io::Write; use std::path::Path; @@ -46,27 +48,45 @@ pub fn create_gzipped_tarball( let mut paths = HashSet::new(); - let mut iterator = walkdir::WalkDir::new(dir).follow_links(false).into_iter(); - while let Some(entry) = iterator.next() { + let mut ob = OverrideBuilder::new(dir); + ob.add("!.git")?.add("!node_modules")?.add("!.DS_Store")?; + + for pattern in file_patterns.as_ref().iter().flat_map(|p| p.include.iter()) { + for path_or_pat in pattern.inner() { + match path_or_pat { + PathOrPattern::Path(p) => ob.add(p.to_str().unwrap())?, + PathOrPattern::Pattern(p) => ob.add(p.as_str())?, + PathOrPattern::RemoteUrl(_) => continue, + }; + } + } + + let overrides = ob.build()?; + + let iterator = WalkBuilder::new(dir) + .follow_links(false) + .require_git(false) + .git_ignore(true) + .git_global(true) + .git_exclude(true) + .overrides(overrides) + .filter_entry(move |entry| { + let matches_pattern = file_patterns + .as_ref() + .map(|p| p.matches_path(entry.path())) + .unwrap_or(true); + matches_pattern + }) + .build(); + + for entry in iterator { let entry = entry?; let path = entry.path(); - let file_type = entry.file_type(); - - let matches_pattern = file_patterns - .as_ref() - .map(|p| p.matches_path(path)) - .unwrap_or(true); - if !matches_pattern - || path.file_name() == Some(OsStr::new(".git")) - || path.file_name() == Some(OsStr::new("node_modules")) - || path.file_name() == Some(OsStr::new(".DS_Store")) - { - if file_type.is_dir() { - iterator.skip_current_dir(); - } + let Some(file_type) = entry.file_type() else { + // entry doesn’t have a file type if it corresponds to stdin. continue; - } + }; let Ok(specifier) = Url::from_file_path(path) else { diagnostics_collector diff --git a/tests/integration/publish_tests.rs b/tests/integration/publish_tests.rs index 61cb40fba5..71bc838a85 100644 --- a/tests/integration/publish_tests.rs +++ b/tests/integration/publish_tests.rs @@ -215,6 +215,43 @@ itest!(config_flag { http_server: true, }); +#[test] +fn ignores_gitignore() { + let context = publish_context_builder().build(); + let temp_dir = context.temp_dir().path(); + temp_dir.join("deno.json").write_json(&json!({ + "name": "@foo/bar", + "version": "1.0.0", + "exports": "./main.ts" + })); + + temp_dir.join("main.ts").write("import './sub_dir/b.ts';"); + + let gitignore = temp_dir.join(".gitignore"); + gitignore.write("ignored.ts\nsub_dir/ignored.wasm"); + + let sub_dir = temp_dir.join("sub_dir"); + sub_dir.create_dir_all(); + sub_dir.join("ignored.wasm").write(""); + sub_dir.join("b.ts").write("export default {}"); + + temp_dir.join("ignored.ts").write(""); + + let output = context + .new_command() + .arg("publish") + .arg("--dry-run") + .arg("--token") + .arg("sadfasdf") + .run(); + output.assert_exit_code(0); + let output = output.combined_output(); + assert_contains!(output, "b.ts"); + assert_contains!(output, "main.ts"); + assert_not_contains!(output, "ignored.ts"); + assert_not_contains!(output, "ignored.wasm"); +} + #[test] fn ignores_directories() { let context = publish_context_builder().build(); @@ -260,6 +297,35 @@ fn ignores_directories() { assert_not_contains!(output, "ignored.ts"); } +#[test] +fn includes_directories_with_gitignore() { + let context = publish_context_builder().build(); + let temp_dir = context.temp_dir().path(); + temp_dir.join("deno.json").write_json(&json!({ + "name": "@foo/bar", + "version": "1.0.0", + "exports": "./main.ts", + "publish": { + "include": [ "deno.json", "main.ts" ] + } + })); + + temp_dir.join(".gitignore").write("main.ts"); + temp_dir.join("main.ts").write(""); + temp_dir.join("ignored.ts").write(""); + + let output = context + .new_command() + .arg("publish") + .arg("--token") + .arg("sadfasdf") + .run(); + output.assert_exit_code(0); + let output = output.combined_output(); + assert_contains!(output, "main.ts"); + assert_not_contains!(output, "ignored.ts"); +} + #[test] fn includes_directories() { let context = publish_context_builder().build(); @@ -279,7 +345,6 @@ fn includes_directories() { let output = context .new_command() .arg("publish") - .arg("--log-level=debug") .arg("--token") .arg("sadfasdf") .run();