fast_code_owners 0.0.10 → 0.0.11

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 53e3384e2f1c996ba3bd2b1520b1128bf4493d41e82dac06dfc4aa45aa801206
4
- data.tar.gz: b8f53467471fc2fe763e686217994fed8e88cbdc419a4f57153f725af0bdfb30
3
+ metadata.gz: 1380e7d12f10242ac7d11dc6f00a9bfdbf83e8d8157522fbd474ea80027b7902
4
+ data.tar.gz: 9ee1e96b93188cd9e788e479f19fdacde71854bdc18c238739531e3083224e44
5
5
  SHA512:
6
- metadata.gz: 4df54cb92903fb958be9b4d21cfe327314ed7785a3a0e14c43a6fc3c026eccfea0d23b7e5659a219b3df626dffd4d72b4871eaf429f7a02d9f3547c26d5bf6d7
7
- data.tar.gz: 4e2b9d0db2f3a57ee9ddb91fc0c03e5816df21a9f3f5182031c0f81c773c91baa0e769039f584a6f39175dd9cd9a760174a65160624ff6804b850430d2d4419f
6
+ metadata.gz: e48a93e1aa738e3a9a1ecbe1ab8b13b704a7c41a99605ce0ce946ccd20e38ec86d10204ed371d6d14c3c269f12501a168c8a5489350a70484c851eb05aa02a06
7
+ data.tar.gz: dc6a57424e0e1312ebe44aa42a66f7d7b6b8b2e423f11a122acdb034fc10a8906a5d538934b87e8ea216b70b36b052d82bea12b19662f9c5a45eb96fd5a25e45
data/.cargo/config CHANGED
@@ -1,9 +1,9 @@
1
1
  [source.crates-io]
2
2
  replace-with = "vendored-sources"
3
3
 
4
- [source."git+https://github.com/rubyatscale/codeowners-rs.git?branch=ph.fast-file-check"]
4
+ [source."git+https://github.com/rubyatscale/codeowners-rs.git?branch=ph.blazing-fast-for-file"]
5
5
  git = "https://github.com/rubyatscale/codeowners-rs.git"
6
- branch = "ph.fast-file-check"
6
+ branch = "ph.blazing-fast-for-file"
7
7
  replace-with = "vendored-sources"
8
8
 
9
9
  [source.vendored-sources]
data/Cargo.lock CHANGED
@@ -189,7 +189,7 @@ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
189
189
  [[package]]
190
190
  name = "codeowners"
191
191
  version = "0.2.5"
192
- source = "git+https://github.com/rubyatscale/codeowners-rs.git?branch=ph.fast-file-check#e036be557cf8a91bb9259bd0f06926ef22bc1441"
192
+ source = "git+https://github.com/rubyatscale/codeowners-rs.git?branch=ph.blazing-fast-for-file#66c46e9b63c415814432205881e7032b53424009"
193
193
  dependencies = [
194
194
  "clap",
195
195
  "clap_derive",
@@ -1 +1 @@
1
- {"files":{".github/workflows/audit.yml":"bde77859031fa481da1fcaf399129b64819d78a0ffbbb2d4c4f5346f4a4dec7e",".github/workflows/ci.yml":"a4b503d5b650f4d59d7e02b3a33f623c373f1ae8c892b6ddcc47b271b7d109db",".github/workflows/dotslash-config.json":"21dbdefc13a01f3aad42923963b9025907d4049d8e75b18208e887f9e601e381",".rustfmt.toml":"394616646760b18ed094433d39e18b7d94986347c21790708bff9ab001f38e0b",".rusty-hook.toml":"9d3d8a8f4f20d80682ce71889a17eb5ba204f4636f27211fafcb6c6b5af5c2bb","Cargo.lock":"13b3503427ab09ad422f8c79bdc03993c2bb89041ae4a9145380321436b5f185","Cargo.toml":"f7c6017dc1d06e2131fbe10a76b0f93fa1253e48496643e55d6899619ad738f7","README.md":"84c2b0f58f5880062b3668fc49bccc8300be209b448340a7fde075bdce5e6fb5","ai-prompts/for-file-optimization.md":"e4c758898de368b9864174fa9bcf0696812b0829e11abda3ef823c4056e237be","dev/run_benchmarks.sh":"81f905201c41e661e4c8208a17f8a4f942b9dcf8becb7b27cadb4ee83283164b","rust-toolchain.toml":"2376c9760d9e4f7ad6a1c27ab074f466872a3b976da1a1652967c6a5eaa65f28","src/bin/compare_for_file.rs":"3ab4adb258f6c6eafcc3689a03d48d9561560e1330c23ba4ef9e8f37645f8ce5","src/cache/file.rs":"b806ba1c8f6f97f6185fd41862da00f8687a8d252b0cc1e8f325b1c9761f6d42","src/cache/mod.rs":"622ad6effc3260c922fe49f3800e0d1c5b3c6a73c81d567296ea6cb550792d32","src/cache/noop.rs":"abfddf1c20cbaca5dd432952d0ff0a0a1d92eb0070866d343e368f29cdd22de9","src/cli.rs":"8e883c291369dd72eff33884db6433a78ef84d902e5f9f475b8a85c50cc071dc","src/common_test.rs":"dfa10d48dc2c5e63b029bfb56ab2d653fc9d07e6ee37859383b9a1d2f55e6c16","src/config.rs":"ad0046ad359dc545c6f06ba61d08e773b7074a5277f8c06e4b34d134f9bbdebc","src/lib.rs":"d48bdfa06836a217b9c0fea3c499e343aea9b50ea939c6b6ad6f2ab664e3da0e","src/main.rs":"ea738dd470e3d8cdf46cbfb397cf5b27c212e0ea94e8d7c2d9c7ea163bbdde65","src/ownership.rs":"22ce2ecab5226a86eccf91cea0c2c679115ddaf764d81910ff001722c391f391","src/ownership/file_generator.rs":"be57f0fe6208bb810bafa59a3c782e8337d099d02728214e932ca2fe620ddb5d","src/ownership/file_owner_finder.rs":"104626da603ddcaccb217387f13141c81fb768b41cf8b7fbc5f65809f487231b","src/ownership/for_file_fast.rs":"4ed19d8dec2b2e1593502a54aaae244a9d600602069fddfe962efb547f788290","src/ownership/mapper.rs":"f313ae6cc6bfe57a4837475655fbce1ea7a715231df05fa15570741431fa53b5","src/ownership/mapper/directory_mapper.rs":"a86866b518efbfdbb056a2d472c981eec875f2ddca5899f029405b76e875b018","src/ownership/mapper/escaper.rs":"2faa41beefedca26d493e8d8f68882384ddbb2c4b108953f30b960847a3f841c","src/ownership/mapper/package_mapper.rs":"290e27c6f8f8d1ff072a6341db3029f70cd5214398c103becd55189a744a7f3e","src/ownership/mapper/team_file_mapper.rs":"d9bb2bd7201ae38c626cd9ceff07b221477980a04f7f56cb3bd961d2729b90e8","src/ownership/mapper/team_gem_mapper.rs":"50b6ad9e60c03e055662724296576b4586167b0d7d6c03819f98b30c0f37342f","src/ownership/mapper/team_glob_mapper.rs":"ce0638118ec9c610210880534a62740f9bd4d15494b0b9516bcc11fa2bbfb508","src/ownership/mapper/team_yml_mapper.rs":"3c759139fc9ff362b82102aeaec958aec7ec9df86df87482a3362ab8ea54404f","src/ownership/parser.rs":"82fc23b22bfe046127b98d875196a406dffaad55e1dd120e3b37a1beb1e54e12","src/ownership/tests.rs":"2d4cf9995be92aab8329b35a7c1181091c08724e24d42527358496d7422df35d","src/ownership/validator.rs":"c710c5f3369b2bbb00c72aa7743a4e908b11dd68bacaf5404d87820f531433d0","src/project.rs":"c44eca21365a9202131ac11f51b38bb9946da9ba39d0ec2c923273107007cedc","src/project_builder.rs":"c048962bec93c1be487cb578c4f9f9254b02fed5f7a55fa2f6414558388c3ba4","src/project_file_builder.rs":"c93ecc878178281180e64a5b01e1934634f85502545ef6478f5252a7c1b67f62","src/runner.rs":"862f61fa9daf4d587f5bb0b5117dbecf7566b8d0ba669df1f12228f12c2d77c0","tests/cache_test.rs":"80b900f38e177eb8c9e162b9373cd1d7c3a551baade716a1b07554a021bfef05","tests/common/mod.rs":"41f2a3a2e886ea2ebc2a58f9b682d4eaa4dc6874297d219364404d00a034d7da","tests/fixtures/invalid_project/.github/CODEOWNERS":"01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b","tests/fixtures/invalid_project/config/code_ownership.yml":"395b9023f51a29b8326a230c82543d8e8e7656376b4d82b1e77c7c5879620a30","tests/fixtures/invalid_project/config/teams/payments.yml":"205b0d200a5b686ad91c4080eadeefef200cc4974939b5ef02c15ef2edf775a1","tests/fixtures/invalid_project/config/teams/payroll.yml":"f3a85ce896837c2a9d38c2d23a600b1fd72f3bf8c4c65c12719ce942b3b17375","tests/fixtures/invalid_project/gems/payroll_calculator/calculator.rb":"350c85edfe24cd54b2479eda5266aae894b4c5eb6e9a3c0a0a1fb30e8c4d1d0f","tests/fixtures/invalid_project/ruby/app/models/bank_account.rb":"0832b2e95f90fa7ebd8dcbfd53c05618f091d38abf5dd238bd4d41645746160d","tests/fixtures/invalid_project/ruby/app/models/blockchain.rb":"9ffaa2304d02b8d40050017b62edbaf61b8600dc0b1a2c5b7f069b7c55c30616","tests/fixtures/invalid_project/ruby/app/models/payroll.rb":"7f48f8730fd738a2760ee81be8a6028cccb3c6c38be0433c7fd81a4bdd631b60","tests/fixtures/invalid_project/ruby/app/payments/nacha.rb":"c5ced2841986f3d48be0a132185a39dbc327715064574887303a25cf02aaad4d","tests/fixtures/invalid_project/ruby/app/services/.codeowner":"53fe8dfb6d9e1b03219adddcc3ffb741557dd579dc653d026097c463def4a8fe","tests/fixtures/invalid_project/ruby/app/services/multi_owned.rb":"42b340c67ad95016eb7e852abe37b39f7a0b5be41f897f972e05d2e581d6c30f","tests/fixtures/invalid_project/ruby/app/unowned.rb":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","tests/fixtures/invalid_project/ruby/packages/payroll_flow/package.yml":"c1a08643821d02a57131870e8b6325f864db0687c5583f3d0872ada9d004072a","tests/fixtures/multiple-directory-owners/.github/CODEOWNERS":"957d160c3d00eb530f7ebb34cc9b7c61934d108eb52ce3cb03b6d9978fad86df","tests/fixtures/multiple-directory-owners/app/consumers/.codeowner":"842c88863910f6eab22edcbdbe9f0f4dd96c6b46c711bd40277b36b8178943c4","tests/fixtures/multiple-directory-owners/app/consumers/deep/nesting/nestdir/deep_file.rb":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","tests/fixtures/multiple-directory-owners/app/consumers/one_owner.rb":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","tests/fixtures/multiple-directory-owners/app/services/.codeowner":"3eae1599bb7f187b86d6427942d172ba8dd7ee5962aab03e0839ad9d59c37eb0","tests/fixtures/multiple-directory-owners/app/services/exciting/.codeowner":"842c88863910f6eab22edcbdbe9f0f4dd96c6b46c711bd40277b36b8178943c4","tests/fixtures/multiple-directory-owners/app/services/exciting/some_other_file.rb":"c0cf1112663d326ba9b47fbcf961e98170c32e9287d5af574c35b3350fb3c59a","tests/fixtures/multiple-directory-owners/config/code_ownership.yml":"fc9679f69ed09090e104341c7d6635c050e81472370acb504a3a42040a48332f","tests/fixtures/multiple-directory-owners/config/teams/bar.yml":"253d9d9cb7f5b5cb81af19d8af7631b510d0f4ad59b32465c5e26bd78886f9c6","tests/fixtures/multiple-directory-owners/config/teams/foo.yml":"d4ba6d61c16e4227681f13013775e3d117e0cee4631714849405c47ff36ecbfc","tests/fixtures/valid_project/.github/CODEOWNERS":"c8e86365c334ac5aeb1d41a579a7f20236b79b4c0cd0e706b8d48a91fb31b641","tests/fixtures/valid_project/.ignore":"68f30767f9c87c700952d52d9d39693e1a98efb6c07a0b9216fbc87d173ebb5e","tests/fixtures/valid_project/config/code_ownership.yml":"a336d56f500203588631ee9f48101f99adf476a3fd786d74cb30e6c9060ae3b3","tests/fixtures/valid_project/config/teams/payments.yml":"7c4edf4ab7db59c1ef13ef4e7aa498c8e8c23c19427ebebe4f43bca003619f2a","tests/fixtures/valid_project/config/teams/payroll.yml":"f3a85ce896837c2a9d38c2d23a600b1fd72f3bf8c4c65c12719ce942b3b17375","tests/fixtures/valid_project/config/teams/ux.yml":"918e96ab1b5c67c99b30b8e984c2cef8fa956b1c83802a76923b3f448bcdec62","tests/fixtures/valid_project/gems/payroll_calculator/calculator.rb":"991056cdd4824eea15055f9c055c22fb3281cc21cffbca94724b330b8a450e59","tests/fixtures/valid_project/gems/pets/dog.rb":"ed2553442d1646af1c421a7a04b92269612efbc5ec0ccb32930e27ceac65b2ea","tests/fixtures/valid_project/javascript/packages/PayrollFlow/index.tsx":"b78b223d3bdb83af6ef7f78bd305f6e18f11abf961ded3266f7bebdb9eb00686","tests/fixtures/valid_project/javascript/packages/PayrollFlow/package.json":"271a0bb50784a65df66fd5db7167d1dd67430c774c207eda58a96974dbafc408","tests/fixtures/valid_project/javascript/packages/items/(special)/.codeowner":"632e1c5839411a1009ef268e992c4b183cb1d5381b198eac0f8323213a61ed82","tests/fixtures/valid_project/javascript/packages/items/(special)/pay.ts":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","tests/fixtures/valid_project/javascript/packages/items/.codeowner":"53fe8dfb6d9e1b03219adddcc3ffb741557dd579dc653d026097c463def4a8fe","tests/fixtures/valid_project/javascript/packages/items/item.ts":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","tests/fixtures/valid_project/ruby/app/models/bank_account.rb":"0832b2e95f90fa7ebd8dcbfd53c05618f091d38abf5dd238bd4d41645746160d","tests/fixtures/valid_project/ruby/app/models/payroll.rb":"7f48f8730fd738a2760ee81be8a6028cccb3c6c38be0433c7fd81a4bdd631b60","tests/fixtures/valid_project/ruby/app/payments/foo/.codeowner":"53fe8dfb6d9e1b03219adddcc3ffb741557dd579dc653d026097c463def4a8fe","tests/fixtures/valid_project/ruby/app/payments/foo/ownedby_payroll.rb":"705296c5ae6b3c1efdb410c952095b6efba3f9edd0af1b42d20e6ba217b1744d","tests/fixtures/valid_project/ruby/app/payments/nacha.rb":"c5ced2841986f3d48be0a132185a39dbc327715064574887303a25cf02aaad4d","tests/fixtures/valid_project/ruby/app/payroll/.codeowner":"b58412ec67186588652c88a6c71fd7499e27984e1e587e137426bed8ddd53173","tests/fixtures/valid_project/ruby/app/payroll/payroll.rb":"ff6cb08457469043bd93b4fdbcecc37a29a506641ab486f9cafd319125ff00ca","tests/fixtures/valid_project/ruby/ignored_files/git_ignored.rb":"fb333306f05e93ff821123b1c09d264c5f37f1bd739156d91167789795704671","tests/fixtures/valid_project/ruby/packages/payroll_flow/package.yml":"c1a08643821d02a57131870e8b6325f864db0687c5583f3d0872ada9d004072a","tests/fixtures/valid_project/should_be_ignored/an_ignored_file.rb":"bd5ce0746f05ded73148e184b36741f3846e0af7409b42c16abae54a9220113a","tests/invalid_project_structure_test.rs":"af048ddfa756bc2e187ea885ea533a4b67907c5bcc531a082ffceca0728649b4","tests/invalid_project_test.rs":"e62b8129d1af0a5a66ced9547dd1e67f17c42980e2b975205909a4530ec3b8db","tests/multiple_directory_owners_test.rs":"bee053097f88481454ad38349e23e716bc44874348b5f4b67687004c9cb9e52f","tests/valid_project_test.rs":"17df92c5a07c8f1c2b1f243a43927c7fe3e0cba3573996fbb77bb7a19aecab62","tmp/.gitkeep":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},"package":null}
1
+ {"files":{".github/workflows/audit.yml":"bde77859031fa481da1fcaf399129b64819d78a0ffbbb2d4c4f5346f4a4dec7e",".github/workflows/ci.yml":"a4b503d5b650f4d59d7e02b3a33f623c373f1ae8c892b6ddcc47b271b7d109db",".github/workflows/dotslash-config.json":"21dbdefc13a01f3aad42923963b9025907d4049d8e75b18208e887f9e601e381",".rustfmt.toml":"394616646760b18ed094433d39e18b7d94986347c21790708bff9ab001f38e0b",".rusty-hook.toml":"9d3d8a8f4f20d80682ce71889a17eb5ba204f4636f27211fafcb6c6b5af5c2bb","Cargo.lock":"13b3503427ab09ad422f8c79bdc03993c2bb89041ae4a9145380321436b5f185","Cargo.toml":"97a8a8fafa063e4e721ca8eb297787d0820cc1163e2eb512513ff92fdb9bef13","README.md":"84c2b0f58f5880062b3668fc49bccc8300be209b448340a7fde075bdce5e6fb5","dev/run_benchmarks.sh":"81f905201c41e661e4c8208a17f8a4f942b9dcf8becb7b27cadb4ee83283164b","rust-toolchain.toml":"2376c9760d9e4f7ad6a1c27ab074f466872a3b976da1a1652967c6a5eaa65f28","src/bin/compare_for_file.rs":"3ab4adb258f6c6eafcc3689a03d48d9561560e1330c23ba4ef9e8f37645f8ce5","src/cache/file.rs":"b806ba1c8f6f97f6185fd41862da00f8687a8d252b0cc1e8f325b1c9761f6d42","src/cache/mod.rs":"622ad6effc3260c922fe49f3800e0d1c5b3c6a73c81d567296ea6cb550792d32","src/cache/noop.rs":"abfddf1c20cbaca5dd432952d0ff0a0a1d92eb0070866d343e368f29cdd22de9","src/cli.rs":"f9f8656647ade58b1f975718240b1ea5a85de34c767e9df9ceafa7e1dcf6c546","src/common_test.rs":"dfa10d48dc2c5e63b029bfb56ab2d653fc9d07e6ee37859383b9a1d2f55e6c16","src/config.rs":"ad0046ad359dc545c6f06ba61d08e773b7074a5277f8c06e4b34d134f9bbdebc","src/lib.rs":"d48bdfa06836a217b9c0fea3c499e343aea9b50ea939c6b6ad6f2ab664e3da0e","src/main.rs":"ea738dd470e3d8cdf46cbfb397cf5b27c212e0ea94e8d7c2d9c7ea163bbdde65","src/ownership.rs":"22ce2ecab5226a86eccf91cea0c2c679115ddaf764d81910ff001722c391f391","src/ownership/file_generator.rs":"be57f0fe6208bb810bafa59a3c782e8337d099d02728214e932ca2fe620ddb5d","src/ownership/file_owner_finder.rs":"104626da603ddcaccb217387f13141c81fb768b41cf8b7fbc5f65809f487231b","src/ownership/for_file_fast.rs":"d2b35647eb2ffdda07a815e550262e04d430ac405c073dc4e144e332657063a8","src/ownership/mapper.rs":"f313ae6cc6bfe57a4837475655fbce1ea7a715231df05fa15570741431fa53b5","src/ownership/mapper/directory_mapper.rs":"a86866b518efbfdbb056a2d472c981eec875f2ddca5899f029405b76e875b018","src/ownership/mapper/escaper.rs":"2faa41beefedca26d493e8d8f68882384ddbb2c4b108953f30b960847a3f841c","src/ownership/mapper/package_mapper.rs":"290e27c6f8f8d1ff072a6341db3029f70cd5214398c103becd55189a744a7f3e","src/ownership/mapper/team_file_mapper.rs":"d9bb2bd7201ae38c626cd9ceff07b221477980a04f7f56cb3bd961d2729b90e8","src/ownership/mapper/team_gem_mapper.rs":"50b6ad9e60c03e055662724296576b4586167b0d7d6c03819f98b30c0f37342f","src/ownership/mapper/team_glob_mapper.rs":"ce0638118ec9c610210880534a62740f9bd4d15494b0b9516bcc11fa2bbfb508","src/ownership/mapper/team_yml_mapper.rs":"3c759139fc9ff362b82102aeaec958aec7ec9df86df87482a3362ab8ea54404f","src/ownership/parser.rs":"82fc23b22bfe046127b98d875196a406dffaad55e1dd120e3b37a1beb1e54e12","src/ownership/tests.rs":"2d4cf9995be92aab8329b35a7c1181091c08724e24d42527358496d7422df35d","src/ownership/validator.rs":"c710c5f3369b2bbb00c72aa7743a4e908b11dd68bacaf5404d87820f531433d0","src/project.rs":"367d3ee94960aeb857c821f9b67b812a2d2dd4ad683b865911fb091a29d44ba3","src/project_builder.rs":"cfa11efd86192c7421029d9600b7dda6cfd85249a88c7664b8e27c869c4194d6","src/project_file_builder.rs":"53dc63ce012bfebcab49e50a2ba9b7e40bb88f9d921f38b3aa8ab1bb60d7a609","src/runner.rs":"3bdd9779948228b860c1a57fb6eedc6015954e3bbcc38de64f21ec743b8f7377","tests/cache_test.rs":"80b900f38e177eb8c9e162b9373cd1d7c3a551baade716a1b07554a021bfef05","tests/common/mod.rs":"5b24dc5a6f90be8a5d75dc96d97f3992684f5f8a1f46562245dfddeb86b4232e","tests/fixtures/invalid_project/.github/CODEOWNERS":"01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b","tests/fixtures/invalid_project/config/code_ownership.yml":"395b9023f51a29b8326a230c82543d8e8e7656376b4d82b1e77c7c5879620a30","tests/fixtures/invalid_project/config/teams/payments.yml":"205b0d200a5b686ad91c4080eadeefef200cc4974939b5ef02c15ef2edf775a1","tests/fixtures/invalid_project/config/teams/payroll.yml":"f3a85ce896837c2a9d38c2d23a600b1fd72f3bf8c4c65c12719ce942b3b17375","tests/fixtures/invalid_project/gems/payroll_calculator/calculator.rb":"350c85edfe24cd54b2479eda5266aae894b4c5eb6e9a3c0a0a1fb30e8c4d1d0f","tests/fixtures/invalid_project/ruby/app/models/bank_account.rb":"0832b2e95f90fa7ebd8dcbfd53c05618f091d38abf5dd238bd4d41645746160d","tests/fixtures/invalid_project/ruby/app/models/blockchain.rb":"9ffaa2304d02b8d40050017b62edbaf61b8600dc0b1a2c5b7f069b7c55c30616","tests/fixtures/invalid_project/ruby/app/models/payroll.rb":"7f48f8730fd738a2760ee81be8a6028cccb3c6c38be0433c7fd81a4bdd631b60","tests/fixtures/invalid_project/ruby/app/payments/nacha.rb":"c5ced2841986f3d48be0a132185a39dbc327715064574887303a25cf02aaad4d","tests/fixtures/invalid_project/ruby/app/services/.codeowner":"53fe8dfb6d9e1b03219adddcc3ffb741557dd579dc653d026097c463def4a8fe","tests/fixtures/invalid_project/ruby/app/services/multi_owned.rb":"42b340c67ad95016eb7e852abe37b39f7a0b5be41f897f972e05d2e581d6c30f","tests/fixtures/invalid_project/ruby/app/unowned.rb":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","tests/fixtures/invalid_project/ruby/packages/payroll_flow/package.yml":"c1a08643821d02a57131870e8b6325f864db0687c5583f3d0872ada9d004072a","tests/fixtures/multiple-directory-owners/.github/CODEOWNERS":"957d160c3d00eb530f7ebb34cc9b7c61934d108eb52ce3cb03b6d9978fad86df","tests/fixtures/multiple-directory-owners/app/consumers/.codeowner":"842c88863910f6eab22edcbdbe9f0f4dd96c6b46c711bd40277b36b8178943c4","tests/fixtures/multiple-directory-owners/app/consumers/deep/nesting/nestdir/deep_file.rb":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","tests/fixtures/multiple-directory-owners/app/consumers/one_owner.rb":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","tests/fixtures/multiple-directory-owners/app/services/.codeowner":"3eae1599bb7f187b86d6427942d172ba8dd7ee5962aab03e0839ad9d59c37eb0","tests/fixtures/multiple-directory-owners/app/services/exciting/.codeowner":"842c88863910f6eab22edcbdbe9f0f4dd96c6b46c711bd40277b36b8178943c4","tests/fixtures/multiple-directory-owners/app/services/exciting/some_other_file.rb":"c0cf1112663d326ba9b47fbcf961e98170c32e9287d5af574c35b3350fb3c59a","tests/fixtures/multiple-directory-owners/config/code_ownership.yml":"fc9679f69ed09090e104341c7d6635c050e81472370acb504a3a42040a48332f","tests/fixtures/multiple-directory-owners/config/teams/bar.yml":"253d9d9cb7f5b5cb81af19d8af7631b510d0f4ad59b32465c5e26bd78886f9c6","tests/fixtures/multiple-directory-owners/config/teams/foo.yml":"d4ba6d61c16e4227681f13013775e3d117e0cee4631714849405c47ff36ecbfc","tests/fixtures/valid_project/.github/CODEOWNERS":"c8e86365c334ac5aeb1d41a579a7f20236b79b4c0cd0e706b8d48a91fb31b641","tests/fixtures/valid_project/.ignore":"68f30767f9c87c700952d52d9d39693e1a98efb6c07a0b9216fbc87d173ebb5e","tests/fixtures/valid_project/config/code_ownership.yml":"a336d56f500203588631ee9f48101f99adf476a3fd786d74cb30e6c9060ae3b3","tests/fixtures/valid_project/config/teams/payments.yml":"7c4edf4ab7db59c1ef13ef4e7aa498c8e8c23c19427ebebe4f43bca003619f2a","tests/fixtures/valid_project/config/teams/payroll.yml":"f3a85ce896837c2a9d38c2d23a600b1fd72f3bf8c4c65c12719ce942b3b17375","tests/fixtures/valid_project/config/teams/ux.yml":"918e96ab1b5c67c99b30b8e984c2cef8fa956b1c83802a76923b3f448bcdec62","tests/fixtures/valid_project/gems/payroll_calculator/calculator.rb":"991056cdd4824eea15055f9c055c22fb3281cc21cffbca94724b330b8a450e59","tests/fixtures/valid_project/gems/pets/dog.rb":"ed2553442d1646af1c421a7a04b92269612efbc5ec0ccb32930e27ceac65b2ea","tests/fixtures/valid_project/javascript/packages/PayrollFlow/index.tsx":"b78b223d3bdb83af6ef7f78bd305f6e18f11abf961ded3266f7bebdb9eb00686","tests/fixtures/valid_project/javascript/packages/PayrollFlow/package.json":"271a0bb50784a65df66fd5db7167d1dd67430c774c207eda58a96974dbafc408","tests/fixtures/valid_project/javascript/packages/items/(special)/.codeowner":"632e1c5839411a1009ef268e992c4b183cb1d5381b198eac0f8323213a61ed82","tests/fixtures/valid_project/javascript/packages/items/(special)/pay.ts":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","tests/fixtures/valid_project/javascript/packages/items/.codeowner":"53fe8dfb6d9e1b03219adddcc3ffb741557dd579dc653d026097c463def4a8fe","tests/fixtures/valid_project/javascript/packages/items/item.ts":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","tests/fixtures/valid_project/ruby/app/models/bank_account.rb":"0832b2e95f90fa7ebd8dcbfd53c05618f091d38abf5dd238bd4d41645746160d","tests/fixtures/valid_project/ruby/app/models/payroll.rb":"7f48f8730fd738a2760ee81be8a6028cccb3c6c38be0433c7fd81a4bdd631b60","tests/fixtures/valid_project/ruby/app/payments/foo/.codeowner":"53fe8dfb6d9e1b03219adddcc3ffb741557dd579dc653d026097c463def4a8fe","tests/fixtures/valid_project/ruby/app/payments/foo/ownedby_payroll.rb":"705296c5ae6b3c1efdb410c952095b6efba3f9edd0af1b42d20e6ba217b1744d","tests/fixtures/valid_project/ruby/app/payments/nacha.rb":"c5ced2841986f3d48be0a132185a39dbc327715064574887303a25cf02aaad4d","tests/fixtures/valid_project/ruby/app/payroll/.codeowner":"b58412ec67186588652c88a6c71fd7499e27984e1e587e137426bed8ddd53173","tests/fixtures/valid_project/ruby/app/payroll/payroll.rb":"ff6cb08457469043bd93b4fdbcecc37a29a506641ab486f9cafd319125ff00ca","tests/fixtures/valid_project/ruby/ignored_files/git_ignored.rb":"fb333306f05e93ff821123b1c09d264c5f37f1bd739156d91167789795704671","tests/fixtures/valid_project/ruby/packages/payroll_flow/package.yml":"c1a08643821d02a57131870e8b6325f864db0687c5583f3d0872ada9d004072a","tests/fixtures/valid_project/should_be_ignored/an_ignored_file.rb":"bd5ce0746f05ded73148e184b36741f3846e0af7409b42c16abae54a9220113a","tests/git_stage_test.rs":"b2cc47863571cfcf89da83d7caa5643932a419c330e676faa2eb66860f352272","tests/invalid_project_structure_test.rs":"af048ddfa756bc2e187ea885ea533a4b67907c5bcc531a082ffceca0728649b4","tests/invalid_project_test.rs":"e62b8129d1af0a5a66ced9547dd1e67f17c42980e2b975205909a4530ec3b8db","tests/multiple_directory_owners_test.rs":"bee053097f88481454ad38349e23e716bc44874348b5f4b67687004c9cb9e52f","tests/valid_project_test.rs":"17df92c5a07c8f1c2b1f243a43927c7fe3e0cba3573996fbb77bb7a19aecab62","tmp/.gitkeep":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},"package":null}
@@ -37,6 +37,10 @@ path = "src/bin/compare_for_file.rs"
37
37
  name = "cache_test"
38
38
  path = "tests/cache_test.rs"
39
39
 
40
+ [[test]]
41
+ name = "git_stage_test"
42
+ path = "tests/git_stage_test.rs"
43
+
40
44
  [[test]]
41
45
  name = "invalid_project_structure_test"
42
46
  path = "tests/invalid_project_structure_test.rs"
@@ -26,7 +26,10 @@ enum Command {
26
26
  about = "Generate the CODEOWNERS file and save it to '--codeowners-file-path'.",
27
27
  visible_alias = "g"
28
28
  )]
29
- Generate,
29
+ Generate {
30
+ #[arg(long, short, default_value = "false", help = "Skip staging the CODEOWNERS file")]
31
+ skip_stage: bool,
32
+ },
30
33
 
31
34
  #[clap(
32
35
  about = "Validate the validity of the CODEOWNERS file. A validation failure will exit with a failure code and a detailed output of the validation errors.",
@@ -35,7 +38,10 @@ enum Command {
35
38
  Validate,
36
39
 
37
40
  #[clap(about = "Chains both `generate` and `validate` commands.", visible_alias = "gv")]
38
- GenerateAndValidate,
41
+ GenerateAndValidate {
42
+ #[arg(long, short, default_value = "false", help = "Skip staging the CODEOWNERS file")]
43
+ skip_stage: bool,
44
+ },
39
45
 
40
46
  #[clap(about = "Delete the cache file.", visible_alias = "d")]
41
47
  DeleteCache,
@@ -101,8 +107,8 @@ pub fn cli() -> Result<RunResult, RunnerError> {
101
107
 
102
108
  let runner_result = match args.command {
103
109
  Command::Validate => runner::validate(&run_config, vec![]),
104
- Command::Generate => runner::generate(&run_config),
105
- Command::GenerateAndValidate => runner::generate_and_validate(&run_config, vec![]),
110
+ Command::Generate { skip_stage } => runner::generate(&run_config, !skip_stage),
111
+ Command::GenerateAndValidate { skip_stage } => runner::generate_and_validate(&run_config, vec![], !skip_stage),
106
112
  Command::ForFile { name, fast } => runner::for_file(&run_config, &name, fast),
107
113
  Command::ForTeam { name } => runner::for_team(&run_config, &name),
108
114
  Command::DeleteCache => runner::delete_cache(&run_config),
@@ -6,10 +6,8 @@ use std::{
6
6
 
7
7
  use fast_glob::glob_match;
8
8
  use glob::glob;
9
- use lazy_static::lazy_static;
10
- use regex::Regex;
11
9
 
12
- use crate::{config::Config, project::Team};
10
+ use crate::{config::Config, project::Team, project_file_builder::build_project_file_without_cache};
13
11
 
14
12
  use super::{FileOwner, mapper::Source};
15
13
 
@@ -106,7 +104,7 @@ pub fn find_file_owners(project_root: &Path, config: &Config, file_path: &Path)
106
104
  }
107
105
 
108
106
  fn build_teams_by_name_map(teams: &[Team]) -> HashMap<String, Team> {
109
- let mut map = HashMap::new();
107
+ let mut map = HashMap::with_capacity(teams.len() * 2);
110
108
  for team in teams {
111
109
  map.insert(team.name.clone(), team.clone());
112
110
  map.insert(team.github_team.clone(), team.clone());
@@ -117,7 +115,7 @@ fn build_teams_by_name_map(teams: &[Team]) -> HashMap<String, Team> {
117
115
  fn load_teams(project_root: &Path, team_file_globs: &[String]) -> std::result::Result<Vec<Team>, String> {
118
116
  let mut teams: Vec<Team> = Vec::new();
119
117
  for glob_str in team_file_globs {
120
- let absolute_glob = format!("{}/{}", project_root.display(), glob_str);
118
+ let absolute_glob = project_root.join(glob_str).to_string_lossy().into_owned();
121
119
  let paths = glob(&absolute_glob).map_err(|e| e.to_string())?;
122
120
  for path in paths.flatten() {
123
121
  match Team::from_team_file_path(path.clone()) {
@@ -132,30 +130,14 @@ fn load_teams(project_root: &Path, team_file_globs: &[String]) -> std::result::R
132
130
  Ok(teams)
133
131
  }
134
132
 
135
- lazy_static! {
136
- // Allow optional leading whitespace before the comment marker
137
- static ref TOP_OF_FILE_TEAM_AT_REGEX: Option<Regex> = Regex::new(r#"^\s*(?:#|//)\s*@team\s+(.+)$"#).ok();
138
- static ref TOP_OF_FILE_TEAM_COLON_REGEX: Option<Regex> = Regex::new(r#"(?i)^\s*(?:#|//)\s*team\s*:\s*(.+)$"#).ok();
139
- }
133
+ // no regex: parse cheaply with ASCII-aware checks
140
134
 
141
135
  fn read_top_of_file_team(path: &Path) -> Option<String> {
142
- let content = fs::read_to_string(path).ok()?;
143
- let line = content.lines().next()?;
144
-
145
- if let Some(re) = &*TOP_OF_FILE_TEAM_AT_REGEX {
146
- if let Some(cap) = re.captures(line) {
147
- if let Some(m) = cap.get(1) {
148
- return Some(m.as_str().to_string());
149
- }
150
- }
151
- }
152
- if let Some(re) = &*TOP_OF_FILE_TEAM_COLON_REGEX {
153
- if let Some(cap) = re.captures(line) {
154
- if let Some(m) = cap.get(1) {
155
- return Some(m.as_str().to_string());
156
- }
157
- }
136
+ let project_file = build_project_file_without_cache(&path.to_path_buf());
137
+ if let Some(owner) = project_file.owner {
138
+ return Some(owner);
158
139
  }
140
+
159
141
  None
160
142
  }
161
143
 
@@ -167,34 +149,32 @@ fn most_specific_directory_owner(
167
149
  let mut current = project_root.join(relative_file_path);
168
150
  let mut best: Option<(String, Source)> = None;
169
151
  loop {
170
- let parent_opt = current.parent().map(|p| p.to_path_buf());
171
- let Some(parent) = parent_opt else { break };
172
- let codeowner_path = parent.join(".codeowner");
152
+ if !current.pop() {
153
+ break;
154
+ }
155
+ let codeowner_path = current.join(".codeowner");
173
156
  if let Ok(owner_str) = fs::read_to_string(&codeowner_path) {
174
157
  let owner = owner_str.trim();
175
158
  if let Some(team) = teams_by_name.get(owner) {
176
- let relative_dir = parent
159
+ let relative_dir = current
177
160
  .strip_prefix(project_root)
178
- .unwrap_or(parent.as_path())
161
+ .unwrap_or(current.as_path())
179
162
  .to_string_lossy()
180
163
  .to_string();
181
164
  let candidate = (team.name.clone(), Source::Directory(relative_dir));
182
165
  match &best {
183
166
  None => best = Some(candidate),
184
167
  Some((_, existing_source)) => {
185
- let existing_len = source_directory_depth(existing_source);
186
- let candidate_len = source_directory_depth(&candidate.1);
187
- if candidate_len > existing_len {
168
+ if candidate.1.len() > existing_source.len() {
188
169
  best = Some(candidate);
189
170
  }
190
171
  }
191
172
  }
192
173
  }
193
174
  }
194
- if parent == project_root {
175
+ if current == project_root {
195
176
  break;
196
177
  }
197
- current = parent.clone();
198
178
  }
199
179
  best
200
180
  }
@@ -207,17 +187,18 @@ fn nearest_package_owner(
207
187
  ) -> Option<(String, Source)> {
208
188
  let mut current = project_root.join(relative_file_path);
209
189
  loop {
210
- let parent_opt = current.parent().map(|p| p.to_path_buf());
211
- let Some(parent) = parent_opt else { break };
212
- let parent_rel = parent.strip_prefix(project_root).unwrap_or(parent.as_path());
190
+ if !current.pop() {
191
+ break;
192
+ }
193
+ let parent_rel = current.strip_prefix(project_root).unwrap_or(current.as_path());
213
194
  if let Some(rel_str) = parent_rel.to_str() {
214
195
  if glob_list_matches(rel_str, &config.ruby_package_paths) {
215
- let pkg_yml = parent.join("package.yml");
196
+ let pkg_yml = current.join("package.yml");
216
197
  if pkg_yml.exists() {
217
198
  if let Ok(owner) = read_ruby_package_owner(&pkg_yml) {
218
199
  if let Some(team) = teams_by_name.get(&owner) {
219
200
  let package_path = parent_rel.join("package.yml");
220
- let package_glob = format!("{}/**/**", rel_str);
201
+ let package_glob = format!("{rel_str}/**/**");
221
202
  return Some((
222
203
  team.name.clone(),
223
204
  Source::Package(package_path.to_string_lossy().to_string(), package_glob),
@@ -227,12 +208,12 @@ fn nearest_package_owner(
227
208
  }
228
209
  }
229
210
  if glob_list_matches(rel_str, &config.javascript_package_paths) {
230
- let pkg_json = parent.join("package.json");
211
+ let pkg_json = current.join("package.json");
231
212
  if pkg_json.exists() {
232
213
  if let Ok(owner) = read_js_package_owner(&pkg_json) {
233
214
  if let Some(team) = teams_by_name.get(&owner) {
234
215
  let package_path = parent_rel.join("package.json");
235
- let package_glob = format!("{}/**/**", rel_str);
216
+ let package_glob = format!("{rel_str}/**/**");
236
217
  return Some((
237
218
  team.name.clone(),
238
219
  Source::Package(package_path.to_string_lossy().to_string(), package_glob),
@@ -242,20 +223,14 @@ fn nearest_package_owner(
242
223
  }
243
224
  }
244
225
  }
245
- if parent == project_root {
226
+ if current == project_root {
246
227
  break;
247
228
  }
248
- current = parent;
249
229
  }
250
230
  None
251
231
  }
252
232
 
253
- fn source_directory_depth(source: &Source) -> usize {
254
- match source {
255
- Source::Directory(path) => path.matches('/').count(),
256
- _ => 0,
257
- }
258
- }
233
+ // removed: use `Source::len()` instead
259
234
 
260
235
  fn glob_list_matches(path: &str, globs: &[String]) -> bool {
261
236
  globs.iter().any(|g| glob_match(g, path))
@@ -311,3 +286,139 @@ fn source_priority(source: &Source) -> u8 {
311
286
  Source::TeamYml => 5,
312
287
  }
313
288
  }
289
+
290
+ #[cfg(test)]
291
+ mod tests {
292
+ use super::*;
293
+ use crate::project::Team;
294
+ use std::collections::HashMap;
295
+ use tempfile::tempdir;
296
+
297
+ fn build_config_for_temp(frontend_glob: &str, ruby_glob: &str, vendored_path: &str) -> crate::config::Config {
298
+ crate::config::Config {
299
+ owned_globs: vec!["**/*".to_string()],
300
+ ruby_package_paths: vec![ruby_glob.to_string()],
301
+ javascript_package_paths: vec![frontend_glob.to_string()],
302
+ team_file_glob: vec!["config/teams/**/*.yml".to_string()],
303
+ unowned_globs: vec![],
304
+ vendored_gems_path: vendored_path.to_string(),
305
+ cache_directory: "tmp/cache/codeowners".to_string(),
306
+ }
307
+ }
308
+
309
+ fn team_named(name: &str) -> Team {
310
+ Team {
311
+ path: Path::new("config/teams/foo.yml").to_path_buf(),
312
+ name: name.to_string(),
313
+ github_team: format!("@{}Team", name),
314
+ owned_globs: vec![],
315
+ subtracted_globs: vec![],
316
+ owned_gems: vec![],
317
+ avoid_ownership: false,
318
+ }
319
+ }
320
+
321
+ #[test]
322
+ fn test_read_top_of_file_team_parses_at_and_colon_forms() {
323
+ let td = tempdir().unwrap();
324
+
325
+ // @team form
326
+ let file_at = td.path().join("at_form.rb");
327
+ std::fs::write(&file_at, "# @team Payroll\nputs 'x'\n").unwrap();
328
+ assert_eq!(read_top_of_file_team(&file_at), Some("Payroll".to_string()));
329
+ }
330
+
331
+ #[test]
332
+ fn test_most_specific_directory_owner_prefers_deeper() {
333
+ let td = tempdir().unwrap();
334
+ let project_root = td.path();
335
+
336
+ // Build directories
337
+ let deep_dir = project_root.join("a/b/c");
338
+ std::fs::create_dir_all(&deep_dir).unwrap();
339
+ let mid_dir = project_root.join("a/b");
340
+ let top_dir = project_root.join("a");
341
+
342
+ // Write .codeowner files
343
+ std::fs::write(top_dir.join(".codeowner"), "TopTeam").unwrap();
344
+ std::fs::write(mid_dir.join(".codeowner"), "MidTeam").unwrap();
345
+ std::fs::write(deep_dir.join(".codeowner"), "DeepTeam").unwrap();
346
+
347
+ // Build teams_by_name
348
+ let mut tbn: HashMap<String, Team> = HashMap::new();
349
+ for name in ["TopTeam", "MidTeam", "DeepTeam"] {
350
+ let t = team_named(name);
351
+ tbn.insert(t.name.clone(), t);
352
+ }
353
+
354
+ let rel_file = Path::new("a/b/c/file.rb");
355
+ let result = most_specific_directory_owner(project_root, rel_file, &tbn).unwrap();
356
+ match result.1 {
357
+ Source::Directory(path) => {
358
+ assert!(path.ends_with("a/b/c"), "expected deepest directory, got {}", path);
359
+ }
360
+ _ => panic!("expected Directory source"),
361
+ }
362
+ assert_eq!(result.0, "DeepTeam");
363
+ }
364
+
365
+ #[test]
366
+ fn test_nearest_package_owner_ruby_and_js() {
367
+ let td = tempdir().unwrap();
368
+ let project_root = td.path();
369
+ let config = build_config_for_temp("frontend/**/*", "packs/**/*", "vendored");
370
+
371
+ // Ruby package
372
+ let ruby_pkg = project_root.join("packs/payroll");
373
+ std::fs::create_dir_all(&ruby_pkg).unwrap();
374
+ std::fs::write(ruby_pkg.join("package.yml"), "---\nowner: Payroll\n").unwrap();
375
+
376
+ // JS package
377
+ let js_pkg = project_root.join("frontend/flow");
378
+ std::fs::create_dir_all(&js_pkg).unwrap();
379
+ std::fs::write(js_pkg.join("package.json"), r#"{"metadata": {"owner": "UX"}}"#).unwrap();
380
+
381
+ // Teams map
382
+ let mut tbn: HashMap<String, Team> = HashMap::new();
383
+ for name in ["Payroll", "UX"] {
384
+ let t = team_named(name);
385
+ tbn.insert(t.name.clone(), t);
386
+ }
387
+
388
+ // Ruby nearest
389
+ let rel_ruby = Path::new("packs/payroll/app/models/thing.rb");
390
+ let ruby_owner = nearest_package_owner(project_root, rel_ruby, &config, &tbn).unwrap();
391
+ assert_eq!(ruby_owner.0, "Payroll");
392
+ match ruby_owner.1 {
393
+ Source::Package(pkg_path, glob) => {
394
+ assert!(pkg_path.ends_with("packs/payroll/package.yml"));
395
+ assert_eq!(glob, "packs/payroll/**/**");
396
+ }
397
+ _ => panic!("expected Package source for ruby"),
398
+ }
399
+
400
+ // JS nearest
401
+ let rel_js = Path::new("frontend/flow/src/index.ts");
402
+ let js_owner = nearest_package_owner(project_root, rel_js, &config, &tbn).unwrap();
403
+ assert_eq!(js_owner.0, "UX");
404
+ match js_owner.1 {
405
+ Source::Package(pkg_path, glob) => {
406
+ assert!(pkg_path.ends_with("frontend/flow/package.json"));
407
+ assert_eq!(glob, "frontend/flow/**/**");
408
+ }
409
+ _ => panic!("expected Package source for js"),
410
+ }
411
+ }
412
+
413
+ #[test]
414
+ fn test_vendored_gem_owner() {
415
+ let config = build_config_for_temp("frontend/**/*", "packs/**/*", "vendored");
416
+ let mut teams: Vec<Team> = vec![team_named("Payroll")];
417
+ teams[0].owned_gems = vec!["awesome_gem".to_string()];
418
+
419
+ let path = Path::new("vendored/awesome_gem/lib/a.rb");
420
+ let result = vendored_gem_owner(path, &config, &teams).unwrap();
421
+ assert_eq!(result.0, "Payroll");
422
+ matches!(result.1, Source::TeamGem);
423
+ }
424
+ }
@@ -195,3 +195,35 @@ impl Project {
195
195
  result
196
196
  }
197
197
  }
198
+
199
+ #[cfg(test)]
200
+ mod tests {
201
+ use super::*;
202
+
203
+ #[test]
204
+ fn test_vendored_gem_by_name_maps_all_gems() {
205
+ let vg1 = VendoredGem {
206
+ path: PathBuf::from("vendored/a"),
207
+ name: "a".to_string(),
208
+ };
209
+ let vg2 = VendoredGem {
210
+ path: PathBuf::from("vendored/b"),
211
+ name: "b".to_string(),
212
+ };
213
+ let project = Project {
214
+ base_path: PathBuf::from("."),
215
+ files: vec![],
216
+ packages: vec![],
217
+ vendored_gems: vec![vg1.clone(), vg2.clone()],
218
+ teams: vec![],
219
+ codeowners_file_path: PathBuf::from(".github/CODEOWNERS"),
220
+ directory_codeowner_files: vec![],
221
+ teams_by_name: HashMap::new(),
222
+ };
223
+
224
+ let map = project.vendored_gem_by_name();
225
+ assert_eq!(map.len(), 2);
226
+ assert_eq!(map.get("a").unwrap().name, vg1.name);
227
+ assert_eq!(map.get("b").unwrap().name, vg2.name);
228
+ }
229
+ }
@@ -1,11 +1,12 @@
1
1
  use std::{
2
2
  fs::File,
3
3
  path::{Path, PathBuf},
4
+ sync::{Arc, Mutex},
4
5
  };
5
6
 
6
7
  use error_stack::{Result, ResultExt};
7
8
  use fast_glob::glob_match;
8
- use ignore::WalkBuilder;
9
+ use ignore::{WalkBuilder, WalkParallel, WalkState};
9
10
  use rayon::iter::{IntoParallelIterator, ParallelIterator};
10
11
  use tracing::{instrument, warn};
11
12
 
@@ -53,12 +54,29 @@ impl<'a> ProjectBuilder<'a> {
53
54
  let mut entry_types = Vec::with_capacity(INITIAL_VECTOR_CAPACITY);
54
55
  let mut builder = WalkBuilder::new(&self.base_path);
55
56
  builder.hidden(false);
56
- let walkdir = builder.build();
57
+ let walk_parallel: WalkParallel = builder.build_parallel();
57
58
 
58
- for entry in walkdir {
59
- let entry = entry.change_context(Error::Io)?;
59
+ let collected = Arc::new(Mutex::new(Vec::with_capacity(INITIAL_VECTOR_CAPACITY)));
60
+ let collected_for_threads = Arc::clone(&collected);
61
+
62
+ walk_parallel.run(move || {
63
+ let collected = Arc::clone(&collected_for_threads);
64
+ Box::new(move |res| {
65
+ if let Ok(entry) = res {
66
+ if let Ok(mut v) = collected.lock() {
67
+ v.push(entry);
68
+ }
69
+ }
70
+ WalkState::Continue
71
+ })
72
+ });
73
+
74
+ // Process sequentially with &mut self
75
+ let collected_entries = Arc::try_unwrap(collected).unwrap().into_inner().unwrap();
76
+ for entry in collected_entries {
60
77
  entry_types.push(self.build_entry_type(entry)?);
61
78
  }
79
+
62
80
  self.build_project_from_entry_types(entry_types)
63
81
  }
64
82
 
@@ -244,14 +262,11 @@ mod tests {
244
262
 
245
263
  #[test]
246
264
  fn test_matches_globs() {
247
- // should fail because hidden directories are ignored by glob patterns unless explicitly included
248
265
  assert!(matches_globs(Path::new("script/.eslintrc.js"), &[OWNED_GLOB.to_string()]));
249
266
  }
250
267
 
251
268
  #[test]
252
269
  fn test_glob_match() {
253
- // Exposes bug in glob-match https://github.com/devongovett/glob-match/issues/9
254
- // should fail because hidden directories are ignored by glob patterns unless explicitly included
255
270
  assert!(glob_match(OWNED_GLOB, "script/.eslintrc.js"));
256
271
  }
257
272
  }
@@ -47,7 +47,7 @@ impl<'a> ProjectFileBuilder<'a> {
47
47
  }
48
48
  }
49
49
 
50
- fn build_project_file_without_cache(path: &PathBuf) -> ProjectFile {
50
+ pub(crate) fn build_project_file_without_cache(path: &PathBuf) -> ProjectFile {
51
51
  let content = match std::fs::read_to_string(path) {
52
52
  Ok(content) => content,
53
53
  Err(_) => {
@@ -2,6 +2,7 @@ use core::fmt;
2
2
  use std::{
3
3
  fs::File,
4
4
  path::{Path, PathBuf},
5
+ process::Command,
5
6
  };
6
7
 
7
8
  use error_stack::{Context, Result, ResultExt};
@@ -144,12 +145,12 @@ pub fn validate(run_config: &RunConfig, _file_paths: Vec<String>) -> RunResult {
144
145
  run_with_runner(run_config, |runner| runner.validate())
145
146
  }
146
147
 
147
- pub fn generate(run_config: &RunConfig) -> RunResult {
148
- run_with_runner(run_config, |runner| runner.generate())
148
+ pub fn generate(run_config: &RunConfig, git_stage: bool) -> RunResult {
149
+ run_with_runner(run_config, |runner| runner.generate(git_stage))
149
150
  }
150
151
 
151
- pub fn generate_and_validate(run_config: &RunConfig, _file_paths: Vec<String>) -> RunResult {
152
- run_with_runner(run_config, |runner| runner.generate_and_validate())
152
+ pub fn generate_and_validate(run_config: &RunConfig, _file_paths: Vec<String>, git_stage: bool) -> RunResult {
153
+ run_with_runner(run_config, |runner| runner.generate_and_validate(git_stage))
153
154
  }
154
155
 
155
156
  pub fn delete_cache(run_config: &RunConfig) -> RunResult {
@@ -253,13 +254,18 @@ impl Runner {
253
254
  }
254
255
  }
255
256
 
256
- pub fn generate(&self) -> RunResult {
257
+ pub fn generate(&self, git_stage: bool) -> RunResult {
257
258
  let content = self.ownership.generate_file();
258
259
  if let Some(parent) = &self.run_config.codeowners_file_path.parent() {
259
260
  let _ = std::fs::create_dir_all(parent);
260
261
  }
261
262
  match std::fs::write(&self.run_config.codeowners_file_path, content) {
262
- Ok(_) => RunResult::default(),
263
+ Ok(_) => {
264
+ if git_stage {
265
+ self.git_stage();
266
+ }
267
+ RunResult::default()
268
+ }
263
269
  Err(err) => RunResult {
264
270
  io_errors: vec![err.to_string()],
265
271
  ..Default::default()
@@ -267,14 +273,22 @@ impl Runner {
267
273
  }
268
274
  }
269
275
 
270
- pub fn generate_and_validate(&self) -> RunResult {
271
- let run_result = self.generate();
276
+ pub fn generate_and_validate(&self, git_stage: bool) -> RunResult {
277
+ let run_result = self.generate(git_stage);
272
278
  if run_result.has_errors() {
273
279
  return run_result;
274
280
  }
275
281
  self.validate()
276
282
  }
277
283
 
284
+ fn git_stage(&self) {
285
+ let _ = Command::new("git")
286
+ .arg("add")
287
+ .arg(&self.run_config.codeowners_file_path)
288
+ .current_dir(&self.run_config.project_root)
289
+ .output();
290
+ }
291
+
278
292
  // TODO: remove this once we've verified the fast path is working
279
293
  #[allow(dead_code)]
280
294
  pub fn for_file(&self, file_path: &str) -> RunResult {
@@ -1,4 +1,9 @@
1
1
  use std::fs;
2
+ use std::path::Path;
3
+ use std::process::Command;
4
+
5
+ use codeowners::runner::{self, RunConfig};
6
+ use tempfile::TempDir;
2
7
 
3
8
  #[allow(dead_code)]
4
9
  pub fn teardown() {
@@ -11,3 +16,98 @@ pub fn teardown() {
11
16
  }
12
17
  });
13
18
  }
19
+
20
+ #[allow(dead_code)]
21
+ pub fn copy_dir_recursive(from: &Path, to: &Path) {
22
+ fs::create_dir_all(to).expect("failed to create destination root");
23
+ for entry in fs::read_dir(from).expect("failed to read source dir") {
24
+ let entry = entry.expect("failed to read dir entry");
25
+ let file_type = entry.file_type().expect("failed to read file type");
26
+ let src_path = entry.path();
27
+ let dest_path = to.join(entry.file_name());
28
+ if file_type.is_dir() {
29
+ copy_dir_recursive(&src_path, &dest_path);
30
+ } else if file_type.is_file() {
31
+ if let Some(parent) = dest_path.parent() {
32
+ fs::create_dir_all(parent).expect("failed to create parent dir");
33
+ }
34
+ fs::copy(&src_path, &dest_path).expect("failed to copy file");
35
+ }
36
+ }
37
+ }
38
+
39
+ #[allow(dead_code)]
40
+ pub fn init_git_repo(path: &Path) {
41
+ let status = Command::new("git")
42
+ .arg("init")
43
+ .current_dir(path)
44
+ .output()
45
+ .expect("failed to run git init");
46
+ assert!(
47
+ status.status.success(),
48
+ "git init failed: {}",
49
+ String::from_utf8_lossy(&status.stderr)
50
+ );
51
+
52
+ let _ = Command::new("git")
53
+ .arg("config")
54
+ .arg("user.email")
55
+ .arg("test@example.com")
56
+ .current_dir(path)
57
+ .output();
58
+ let _ = Command::new("git")
59
+ .arg("config")
60
+ .arg("user.name")
61
+ .arg("Test User")
62
+ .current_dir(path)
63
+ .output();
64
+ }
65
+
66
+ #[allow(dead_code)]
67
+ pub fn is_file_staged(repo_root: &Path, rel_path: &str) -> bool {
68
+ let output = Command::new("git")
69
+ .arg("diff")
70
+ .arg("--name-only")
71
+ .arg("--cached")
72
+ .current_dir(repo_root)
73
+ .output()
74
+ .expect("failed to run git diff --cached");
75
+ assert!(
76
+ output.status.success(),
77
+ "git diff failed: {}",
78
+ String::from_utf8_lossy(&output.stderr)
79
+ );
80
+ let stdout = String::from_utf8_lossy(&output.stdout);
81
+ stdout.lines().any(|line| line.trim() == rel_path)
82
+ }
83
+
84
+ #[allow(dead_code)]
85
+ pub fn build_run_config(project_root: &Path, codeowners_rel_path: &str) -> RunConfig {
86
+ let project_root = project_root.canonicalize().expect("failed to canonicalize project root");
87
+ let codeowners_file_path = project_root.join(codeowners_rel_path);
88
+ let config_path = project_root.join("config/code_ownership.yml");
89
+ RunConfig {
90
+ project_root,
91
+ codeowners_file_path,
92
+ config_path,
93
+ no_cache: true,
94
+ }
95
+ }
96
+
97
+ #[allow(dead_code)]
98
+ pub fn setup_fixture_repo(fixture_root: &Path) -> TempDir {
99
+ let temp_dir = tempfile::tempdir().expect("failed to create tempdir");
100
+ copy_dir_recursive(fixture_root, temp_dir.path());
101
+ init_git_repo(temp_dir.path());
102
+ temp_dir
103
+ }
104
+
105
+ #[allow(dead_code)]
106
+ pub fn assert_no_run_errors(result: &runner::RunResult) {
107
+ assert!(result.io_errors.is_empty(), "io_errors: {:?}", result.io_errors);
108
+ assert!(
109
+ result.validation_errors.is_empty(),
110
+ "validation_errors: {:?}",
111
+ result.validation_errors
112
+ );
113
+ }
@@ -0,0 +1,44 @@
1
+ use std::path::Path;
2
+
3
+ use codeowners::runner::{self, RunConfig};
4
+
5
+ mod common;
6
+ use common::{assert_no_run_errors, build_run_config, is_file_staged, setup_fixture_repo};
7
+
8
+ #[test]
9
+ fn test_generate_stages_codeowners() {
10
+ run_and_check(runner::generate, true, true);
11
+ }
12
+
13
+ #[test]
14
+ fn test_generate_and_validate_stages_codeowners() {
15
+ run_and_check(|rc, s| runner::generate_and_validate(rc, vec![], s), true, true);
16
+ }
17
+
18
+ #[test]
19
+ fn test_generate_does_not_stage_codeowners() {
20
+ run_and_check(runner::generate, false, false);
21
+ }
22
+
23
+ #[test]
24
+ fn test_generate_and_validate_does_not_stage_codeowners() {
25
+ run_and_check(|rc, s| runner::generate_and_validate(rc, vec![], s), false, false);
26
+ }
27
+
28
+ const FIXTURE: &str = "tests/fixtures/valid_project";
29
+ const CODEOWNERS_REL: &str = ".github/CODEOWNERS";
30
+
31
+ fn run_and_check<F>(func: F, stage: bool, expected_staged: bool)
32
+ where
33
+ F: FnOnce(&RunConfig, bool) -> runner::RunResult,
34
+ {
35
+ let temp_dir = setup_fixture_repo(Path::new(FIXTURE));
36
+ let run_config = build_run_config(temp_dir.path(), CODEOWNERS_REL);
37
+
38
+ let result = func(&run_config, stage);
39
+ assert_no_run_errors(&result);
40
+
41
+ assert!(run_config.codeowners_file_path.exists(), "CODEOWNERS file was not created");
42
+ let staged = is_file_staged(&run_config.project_root, CODEOWNERS_REL);
43
+ assert_eq!(staged, expected_staged, "unexpected staged state for CODEOWNERS");
44
+ }
@@ -17,7 +17,7 @@ rb-sys = { version = "0.9.117", features = [
17
17
  magnus = { version = "0.7.1" }
18
18
  serde = { version = "1.0.219", features = ["derive"] }
19
19
  serde_magnus = "0.9.0"
20
- codeowners = { git = "https://github.com/rubyatscale/codeowners-rs.git", branch = "ph.fast-file-check" }
20
+ codeowners = { git = "https://github.com/rubyatscale/codeowners-rs.git", branch = "ph.blazing-fast-for-file" }
21
21
 
22
22
  [dev-dependencies]
23
23
  rb-sys = { version = "0.9.117", features = [
@@ -43,9 +43,9 @@ fn validate() -> Result<Value, Error> {
43
43
  validate_result(&run_result)
44
44
  }
45
45
 
46
- fn generate_and_validate() -> Result<Value, Error> {
46
+ fn generate_and_validate(skip_stage: bool) -> Result<Value, Error> {
47
47
  let run_config = build_run_config();
48
- let run_result = runner::generate_and_validate(&run_config, vec![]);
48
+ let run_result = runner::generate_and_validate(&run_config, vec![], skip_stage);
49
49
  validate_result(&run_result)
50
50
  }
51
51
 
@@ -85,7 +85,7 @@ fn build_run_config() -> RunConfig {
85
85
  fn init(ruby: &Ruby) -> Result<(), Error> {
86
86
  let module = ruby.define_module("RustCodeOwners")?;
87
87
  module.define_singleton_method("for_file", function!(for_file, 1))?;
88
- module.define_singleton_method("generate_and_validate", function!(generate_and_validate, 0))?;
88
+ module.define_singleton_method("generate_and_validate", function!(generate_and_validate, 1))?;
89
89
  module.define_singleton_method("validate", function!(validate, 0))?;
90
90
  module.define_singleton_method("for_team", function!(for_team, 1))?;
91
91
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FastCodeOwners
4
- VERSION = '0.0.10'
4
+ VERSION = '0.0.11'
5
5
  end
@@ -38,7 +38,7 @@ module FastCodeOwners
38
38
  files: nil
39
39
  )
40
40
  if autocorrect
41
- ::RustCodeOwners.generate_and_validate
41
+ ::RustCodeOwners.generate_and_validate(!stage_changes)
42
42
  else
43
43
  ::RustCodeOwners.validate
44
44
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fast_code_owners
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.10
4
+ version: 0.0.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Perry Hertler
@@ -841,7 +841,6 @@ files:
841
841
  - "./ext/cargo-vendor/codeowners-0.2.5/Cargo.lock"
842
842
  - "./ext/cargo-vendor/codeowners-0.2.5/Cargo.toml"
843
843
  - "./ext/cargo-vendor/codeowners-0.2.5/README.md"
844
- - "./ext/cargo-vendor/codeowners-0.2.5/ai-prompts/for-file-optimization.md"
845
844
  - "./ext/cargo-vendor/codeowners-0.2.5/dev/run_benchmarks.sh"
846
845
  - "./ext/cargo-vendor/codeowners-0.2.5/rust-toolchain.toml"
847
846
  - "./ext/cargo-vendor/codeowners-0.2.5/src/bin/compare_for_file.rs"
@@ -921,6 +920,7 @@ files:
921
920
  - "./ext/cargo-vendor/codeowners-0.2.5/tests/fixtures/valid_project/ruby/ignored_files/git_ignored.rb"
922
921
  - "./ext/cargo-vendor/codeowners-0.2.5/tests/fixtures/valid_project/ruby/packages/payroll_flow/package.yml"
923
922
  - "./ext/cargo-vendor/codeowners-0.2.5/tests/fixtures/valid_project/should_be_ignored/an_ignored_file.rb"
923
+ - "./ext/cargo-vendor/codeowners-0.2.5/tests/git_stage_test.rs"
924
924
  - "./ext/cargo-vendor/codeowners-0.2.5/tests/invalid_project_structure_test.rs"
925
925
  - "./ext/cargo-vendor/codeowners-0.2.5/tests/invalid_project_test.rs"
926
926
  - "./ext/cargo-vendor/codeowners-0.2.5/tests/multiple_directory_owners_test.rs"
@@ -1,21 +0,0 @@
1
- ## Re-architect `codeowners-rs`
2
-
3
- The CLI behaviors live in `src/cli.rs`.
4
-
5
- ### What this tool does
6
- - **Generate CODEOWNERS**: Builds `.github/CODEOWNERS` from multiple ownership sources. See mappers in `src/ownership/mapper/`.
7
- - **Answer per-file ownership**: The `for-file` command returns the owner of a given file even if the checked-in `CODEOWNERS` is not up to date.
8
-
9
- ### Current implementations
10
- - **Fast but potentially inaccurate**: `Runner::for_file_from_codeowners` (in `src/runner.rs`) parses the existing `CODEOWNERS` file to find a file’s owner. It’s very fast, but can be wrong if `CODEOWNERS` is stale.
11
- - **Accurate but slow**: `Runner::for_file` is correct but can take up to ~4 seconds on large repositories because it effectively determines ownership for many files and then returns the single result needed.
12
-
13
- Ownership resolution is complex because definitions often live in other files (packages, team configs, globs, etc.).
14
-
15
- ## Assignment
16
- Implement a faster `for_file` that remains accurate.
17
-
18
- ### Requirements
19
- - **Performance**: Under 1 second on large codebases (e.g., `$HOME/workspace/large`).
20
- - **Correctness**: All existing tests must continue to pass.
21
- - **Compatibility**: No changes to the external behavior of the `for-file` CLI command.