Skip to content

Commit 9cee8f0

Browse files
authored
Refactor runner into façade + modules; extract CODEOWNERS queries; add tests and docs (#79)
* extracting separate concerns from runner * updating README with usage examples * bumping version * DRY * updates to readme * rename to annotated file mapper * js project example * js example * js example
1 parent 37027e3 commit 9cee8f0

File tree

27 files changed

+721
-399
lines changed

27 files changed

+721
-399
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "codeowners"
3-
version = "0.2.15"
3+
version = "0.2.16"
44
edition = "2024"
55

66
[profile.release]

README.md

Lines changed: 139 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,63 @@
11
# Codeowners
22

3-
**Codeowners** is a fast, Rust-based CLI for generating and validating [GitHub `CODEOWNERS` files](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners) in large repositories.
3+
**Codeowners** is a fast, Rust-based CLI for generating and validating [GitHub `CODEOWNERS` files](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners) in large repositories.
44

5-
Note: For Ruby application, it's usually easier to use `codeowners-rs` via the [code_ownership](https://github.com/rubyatscale/code_ownership) gem.
5+
Note: For Ruby applications, it's usually easier to use `codeowners-rs` via the [code_ownership](https://github.com/rubyatscale/code_ownership) gem.
66

77
## 🚀 Quick Start: Generate & Validate
88

9-
The most common workflow is to **generate and validate your CODEOWNERS file** in a single step:
9+
The most common workflow is to generate and validate in one step:
1010

1111
```sh
1212
codeowners gv
1313
```
1414

15-
- This command will:
16-
- Generate a fresh `CODEOWNERS` file (by default at `.github/CODEOWNERS`)
17-
- Validate that all files are properly owned and that the file is up to date
18-
- Exit with a nonzero code and detailed errors if validation fails
15+
- Generates a fresh `CODEOWNERS` file (default: `.github/CODEOWNERS`)
16+
- Validates ownership and that the file is up to date
17+
- Exits non-zero and prints detailed errors if validation fails
1918

2019
## Table of Contents
2120

22-
- [Quick Start: Generate & Validate](#-quick-start-generate--validate)
21+
- [Quick Start: Generate & Validate](#quick-start-generate--validate)
22+
- [Installation](#installation)
2323
- [Getting Started](#getting-started)
2424
- [Declaring Ownership](#declaring-ownership)
2525
- [Directory-Based Ownership](#1-directory-based-ownership)
2626
- [File Annotation](#2-file-annotation)
2727
- [Package-Based Ownership](#3-package-based-ownership)
2828
- [Glob-Based Ownership](#4-glob-based-ownership)
2929
- [JavaScript Package Ownership](#5-javascript-package-ownership)
30-
- [Other CLI Commands](#other-cli-commands)
30+
- [CLI Reference](#cli-reference)
31+
- [Global Flags](#global-flags)
32+
- [Commands](#commands)
3133
- [Examples](#examples)
32-
- [Find the owner of a file](#find-the-owner-of-a-file)
33-
- [Ownership report for a team](#ownership-report-for-a-team)
34+
- [Configuration](#configuration)
35+
- [Cache](#cache)
3436
- [Validation](#validation)
37+
- [Library Usage](#library-usage)
3538
- [Development](#development)
36-
- [Configuration](#configuration)
3739

38-
## Getting Started
40+
## Installation
41+
42+
You can run `codeowners` without installing a platform-specific binary by using DotSlash, or install from source with Cargo.
43+
44+
### Option A: DotSlash (recommended)
3945

40-
1. **Install DotSlash**
41-
[Install DotSlash](https://dotslash-cli.com/docs/installation/)
42-
Releases include a DotSlash text file that will automatically download and run the correct binary for your system.
46+
1. Install DotSlash: see [https://dotslash-cli.com/docs/installation/](https://dotslash-cli.com/docs/installation/)
47+
2. Download the latest DotSlash text file from a release, for example [https://github.com/rubyatscale/codeowners-rs/releases](https://github.com/rubyatscale/codeowners-rs/releases).
48+
3. Execute the downloaded file with DotSlash; it will fetch and run the correct binary.
4349

44-
2. **Download the Latest DotSlash Text File**
45-
Releases contain a DotSlash text file. Example: [codeowners release v0.2.4](https://github.com/rubyatscale/codeowners-rs/releases/download/v0.2.4/codeowners).
46-
Running this file with DotSlash installed will execute `codeowners`.
50+
### Option B: From source with Cargo
4751

48-
3. **Configure Ownership**
52+
Requires Rust toolchain.
53+
54+
```sh
55+
cargo install --git https://github.com/rubyatscale/codeowners-rs codeowners
56+
```
57+
58+
## Getting Started
59+
60+
1. **Configure Ownership**
4961
Create a `config/code_ownership.yml` file. Example:
5062

5163
```yaml
@@ -58,7 +70,7 @@ codeowners gv
5870
- frontend/javascripts/**/__generated__/**/*
5971
```
6072
61-
4. **Declare Teams**
73+
2. **Declare Teams**
6274
Example: `config/teams/operations.yml`
6375

6476
```yaml
@@ -67,7 +79,7 @@ codeowners gv
6779
team: '@my-org/operations-team'
6880
```
6981

70-
5. **Run the Main Workflow**
82+
3. **Run the Main Workflow**
7183

7284
```sh
7385
codeowners gv
@@ -92,7 +104,15 @@ Add an annotation at the top of a file:
92104
```ruby
93105
# @team MyTeam
94106
```
95-
107+
```typescript
108+
// @team MyTeam
109+
```
110+
```html
111+
<!-- @team MyTeam -->
112+
```
113+
```erb
114+
<%# @team: Foo %>
115+
```
96116
### 3. Package-Based Ownership
97117

98118
In `package.yml` (for Ruby Packwerk):
@@ -133,29 +153,27 @@ js_package_paths:
133153
- frontend/javascripts/packages/*
134154
```
135155

156+
## CLI Reference
136157

137-
## Other CLI Commands
158+
### Global Flags
138159

139-
While `codeowners gv` is the main workflow, the CLI also supports:
160+
- `--codeowners-file-path <path>`: Path for the CODEOWNERS file. Default: `./.github/CODEOWNERS`
161+
- `--config-path <path>`: Path to `code_ownership.yml`. Default: `./config/code_ownership.yml`
162+
- `--project-root <path>`: Project root. Default: `.`
163+
- `--no-cache`: Disable on-disk caching (useful in CI)
164+
- `-V, --version`, `-h, --help`
140165

141-
```text
142-
Usage: codeowners [OPTIONS] <COMMAND>
143-
144-
Commands:
145-
for-file Finds the owner of a given file. [aliases: f]
146-
for-team Finds code ownership information for a given team [aliases: t]
147-
generate Generate the CODEOWNERS file [aliases: g]
148-
validate Validate the CODEOWNERS file [aliases: v]
149-
generate-and-validate Chains both `generate` and `validate` [aliases: gv]
150-
help Print this message or the help of the given subcommand(s)
166+
### Commands
151167

152-
Options:
153-
--codeowners-file-path <CODEOWNERS_FILE_PATH> [default: ./.github/CODEOWNERS]
154-
--config-path <CONFIG_PATH> [default: ./config/code_ownership.yml]
155-
--project-root <PROJECT_ROOT> [default: .]
156-
-h, --help
157-
-V, --version
158-
```
168+
- `generate` (`g`): Generate the CODEOWNERS file and write it to `--codeowners-file-path`.
169+
- Flags: `--skip-stage, -s` to avoid `git add` after writing
170+
- `validate` (`v`): Validate the CODEOWNERS file and configuration.
171+
- `generate-and-validate` (`gv`): Run `generate` then `validate`.
172+
- Flags: `--skip-stage, -s`
173+
- `for-file <path>` (`f`): Print the owner of a file.
174+
- Flags: `--from-codeowners` to resolve using only the CODEOWNERS rules
175+
- `for-team <name>` (`t`): Print ownership report for a team.
176+
- `delete-cache` (`d`): Delete the persisted cache.
159177

160178
### Examples
161179

@@ -171,14 +189,83 @@ codeowners for-file path/to/file.rb
171189
codeowners for-team Payroll
172190
```
173191

192+
#### Generate but do not stage the file
193+
194+
```sh
195+
codeowners generate --skip-stage
196+
```
197+
198+
#### Run without using the cache
199+
200+
```sh
201+
codeowners gv --no-cache
202+
```
203+
204+
## Configuration
205+
206+
`config/code_ownership.yml` keys and defaults:
207+
208+
- `owned_globs` (required): Glob patterns that must be owned.
209+
- `ruby_package_paths` (default: `['packs/**/*', 'components/**']`)
210+
- `js_package_paths` / `javascript_package_paths` (default: `['frontend/**/*']`)
211+
- `team_file_glob` (default: `['config/teams/**/*.yml']`)
212+
- `unowned_globs` (default: `['frontend/**/node_modules/**/*', 'frontend/**/__generated__/**/*']`)
213+
- `vendored_gems_path` (default: `'vendored/'`)
214+
- `cache_directory` (default: `'tmp/cache/codeowners'`)
215+
- `ignore_dirs` (default includes: `.git`, `node_modules`, `tmp`, etc.)
216+
217+
See examples in `tests/fixtures/**/config/` for reference setups.
218+
219+
## Cache
220+
221+
By default, cache is stored under `tmp/cache/codeowners` relative to the project root. This speeds up repeated runs.
222+
223+
- Disable cache for a run: add the global flag `--no-cache`
224+
- Clear all cache: `codeowners delete-cache`
225+
174226
## Validation
175227

176228
`codeowners validate` (or `codeowners gv`) ensures:
177229

178230
1. Only one mechanism defines ownership for any file.
179231
2. All referenced teams are valid.
180-
3. All files in `owned_globs` are owned, unless in `unowned_globs`.
181-
4. The `CODEOWNERS` file is up to date.
232+
3. All files in `owned_globs` are owned, unless matched by `unowned_globs`.
233+
4. The generated `CODEOWNERS` file is up to date.
234+
235+
Exit status is non-zero on errors.
236+
237+
## Library Usage
238+
239+
Import public APIs from `codeowners::runner::*`.
240+
241+
```rust
242+
use codeowners::runner::{RunConfig, for_file, teams_for_files_from_codeowners};
243+
244+
fn main() {
245+
let run_config = RunConfig {
246+
project_root: std::path::PathBuf::from("."),
247+
codeowners_file_path: std::path::PathBuf::from(".github/CODEOWNERS"),
248+
config_path: std::path::PathBuf::from("config/code_ownership.yml"),
249+
no_cache: true, // set false to enable on-disk caching
250+
};
251+
252+
// Find owner for a single file using the optimized path (not just CODEOWNERS)
253+
let result = for_file(&run_config, "app/models/user.rb", false);
254+
for msg in result.info_messages { println!("{}", msg); }
255+
for err in result.io_errors { eprintln!("io: {}", err); }
256+
for err in result.validation_errors { eprintln!("validation: {}", err); }
257+
258+
// Map multiple files to teams using CODEOWNERS rules only
259+
let files = vec![
260+
"app/models/user.rb".to_string(),
261+
"config/teams/payroll.yml".to_string(),
262+
];
263+
match teams_for_files_from_codeowners(&run_config, &files) {
264+
Ok(map) => println!("{:?}", map),
265+
Err(e) => eprintln!("error: {}", e),
266+
}
267+
}
268+
```
182269

183270
## Development
184271

@@ -190,3 +277,11 @@ codeowners for-team Payroll
190277
```
191278

192279
- Please update `CHANGELOG.md` and this `README.md` when making changes.
280+
281+
### Module layout
282+
283+
- `src/runner.rs`: public façade re-exporting the API and types.
284+
- `src/runner/api.rs`: externally available functions used by the CLI and other crates.
285+
- `src/runner/types.rs`: `RunConfig`, `RunResult`, and runner `Error`.
286+
- `src/ownership/`: all ownership logic (parsing, mapping, validation, generation).
287+
- `src/ownership/codeowners_query.rs`: CODEOWNERS-only queries consumed by the façade.

src/config.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use serde::Deserialize;
2+
use std::{fs::File, path::Path};
23

34
#[derive(Deserialize, Debug, Clone)]
45
pub struct Config {
@@ -77,6 +78,13 @@ fn default_ignore_dirs() -> Vec<String> {
7778
]
7879
}
7980

81+
impl Config {
82+
pub fn load_from_path(path: &Path) -> std::result::Result<Self, String> {
83+
let file = File::open(path).map_err(|e| format!("Can't open config file: {} ({})", path.to_string_lossy(), e))?;
84+
serde_yaml::from_reader(file).map_err(|e| format!("Can't parse config file: {} ({})", path.to_string_lossy(), e))
85+
}
86+
}
87+
8088
#[cfg(test)]
8189
mod tests {
8290
use std::{

src/crosscheck.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::path::Path;
33
use crate::{
44
cache::Cache,
55
config::Config,
6-
ownership::for_file_fast::find_file_owners,
6+
ownership::file_owner_resolver::find_file_owners,
77
project::Project,
88
project_builder::ProjectBuilder,
99
runner::{RunConfig, RunResult, config_from_path, team_for_file_from_codeowners},

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pub(crate) mod common_test;
33
pub mod config;
44
pub mod crosscheck;
55
pub mod ownership;
6+
pub mod path_utils;
67
pub(crate) mod project;
78
pub mod project_builder;
89
pub mod project_file_builder;

src/ownership.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ use std::{
1010
use tracing::{info, instrument};
1111

1212
pub(crate) mod codeowners_file_parser;
13+
pub(crate) mod codeowners_query;
1314
mod file_generator;
1415
mod file_owner_finder;
15-
pub mod for_file_fast;
16+
pub mod file_owner_resolver;
1617
pub(crate) mod mapper;
1718
mod validator;
1819

src/ownership/codeowners_query.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
use std::collections::HashMap;
2+
use std::path::{Path, PathBuf};
3+
4+
use crate::ownership::codeowners_file_parser::Parser;
5+
use crate::project::Team;
6+
7+
pub(crate) fn team_for_file_from_codeowners(
8+
project_root: &Path,
9+
codeowners_file_path: &Path,
10+
team_file_globs: &[String],
11+
file_path: &Path,
12+
) -> Result<Option<Team>, String> {
13+
let relative_file_path = if file_path.is_absolute() {
14+
crate::path_utils::relative_to_buf(project_root, file_path)
15+
} else {
16+
PathBuf::from(file_path)
17+
};
18+
19+
let parser = Parser {
20+
codeowners_file_path: codeowners_file_path.to_path_buf(),
21+
project_root: project_root.to_path_buf(),
22+
team_file_globs: team_file_globs.to_vec(),
23+
};
24+
25+
parser.team_from_file_path(&relative_file_path).map_err(|e| e.to_string())
26+
}
27+
28+
pub(crate) fn teams_for_files_from_codeowners(
29+
project_root: &Path,
30+
codeowners_file_path: &Path,
31+
team_file_globs: &[String],
32+
file_paths: &[String],
33+
) -> Result<HashMap<String, Option<Team>>, String> {
34+
let relative_file_paths: Vec<PathBuf> = file_paths
35+
.iter()
36+
.map(Path::new)
37+
.map(|path| {
38+
if path.is_absolute() {
39+
crate::path_utils::relative_to_buf(project_root, path)
40+
} else {
41+
path.to_path_buf()
42+
}
43+
})
44+
.collect();
45+
46+
let parser = Parser {
47+
codeowners_file_path: codeowners_file_path.to_path_buf(),
48+
project_root: project_root.to_path_buf(),
49+
team_file_globs: team_file_globs.to_vec(),
50+
};
51+
52+
parser.teams_from_files_paths(&relative_file_paths).map_err(|e| e.to_string())
53+
}

0 commit comments

Comments
 (0)