Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "codeowners"
version = "0.2.15"
version = "0.2.16"
edition = "2024"

[profile.release]
Expand Down
183 changes: 139 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,51 +1,63 @@
# Codeowners

**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.
**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.

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

## 🚀 Quick Start: Generate & Validate

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

```sh
codeowners gv
```

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

## Table of Contents

- [Quick Start: Generate & Validate](#-quick-start-generate--validate)
- [Quick Start: Generate & Validate](#quick-start-generate--validate)
- [Installation](#installation)
- [Getting Started](#getting-started)
- [Declaring Ownership](#declaring-ownership)
- [Directory-Based Ownership](#1-directory-based-ownership)
- [File Annotation](#2-file-annotation)
- [Package-Based Ownership](#3-package-based-ownership)
- [Glob-Based Ownership](#4-glob-based-ownership)
- [JavaScript Package Ownership](#5-javascript-package-ownership)
- [Other CLI Commands](#other-cli-commands)
- [CLI Reference](#cli-reference)
- [Global Flags](#global-flags)
- [Commands](#commands)
- [Examples](#examples)
- [Find the owner of a file](#find-the-owner-of-a-file)
- [Ownership report for a team](#ownership-report-for-a-team)
- [Configuration](#configuration)
- [Cache](#cache)
- [Validation](#validation)
- [Library Usage](#library-usage)
- [Development](#development)
- [Configuration](#configuration)

## Getting Started
## Installation

You can run `codeowners` without installing a platform-specific binary by using DotSlash, or install from source with Cargo.

### Option A: DotSlash (recommended)

1. **Install DotSlash**
[Install DotSlash](https://dotslash-cli.com/docs/installation/)
Releases include a DotSlash text file that will automatically download and run the correct binary for your system.
1. Install DotSlash: see [https://dotslash-cli.com/docs/installation/](https://dotslash-cli.com/docs/installation/)
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).
3. Execute the downloaded file with DotSlash; it will fetch and run the correct binary.

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

3. **Configure Ownership**
Requires Rust toolchain.

```sh
cargo install --git https://github.com/rubyatscale/codeowners-rs codeowners
```

## Getting Started

1. **Configure Ownership**
Create a `config/code_ownership.yml` file. Example:

```yaml
Expand All @@ -58,7 +70,7 @@ codeowners gv
- frontend/javascripts/**/__generated__/**/*
```

4. **Declare Teams**
2. **Declare Teams**
Example: `config/teams/operations.yml`

```yaml
Expand All @@ -67,7 +79,7 @@ codeowners gv
team: '@my-org/operations-team'
```

5. **Run the Main Workflow**
3. **Run the Main Workflow**

```sh
codeowners gv
Expand All @@ -92,7 +104,15 @@ Add an annotation at the top of a file:
```ruby
# @team MyTeam
```

```typescript
// @team MyTeam
```
```html
<!-- @team MyTeam -->
```
```erb
<%# @team: Foo %>
```
### 3. Package-Based Ownership

In `package.yml` (for Ruby Packwerk):
Expand Down Expand Up @@ -133,29 +153,27 @@ js_package_paths:
- frontend/javascripts/packages/*
```

## CLI Reference

## Other CLI Commands
### Global Flags

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

```text
Usage: codeowners [OPTIONS] <COMMAND>

Commands:
for-file Finds the owner of a given file. [aliases: f]
for-team Finds code ownership information for a given team [aliases: t]
generate Generate the CODEOWNERS file [aliases: g]
validate Validate the CODEOWNERS file [aliases: v]
generate-and-validate Chains both `generate` and `validate` [aliases: gv]
help Print this message or the help of the given subcommand(s)
### Commands

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

### Examples

Expand All @@ -171,14 +189,83 @@ codeowners for-file path/to/file.rb
codeowners for-team Payroll
```

#### Generate but do not stage the file

```sh
codeowners generate --skip-stage
```

#### Run without using the cache

```sh
codeowners gv --no-cache
```

## Configuration

`config/code_ownership.yml` keys and defaults:

- `owned_globs` (required): Glob patterns that must be owned.
- `ruby_package_paths` (default: `['packs/**/*', 'components/**']`)
- `js_package_paths` / `javascript_package_paths` (default: `['frontend/**/*']`)
- `team_file_glob` (default: `['config/teams/**/*.yml']`)
- `unowned_globs` (default: `['frontend/**/node_modules/**/*', 'frontend/**/__generated__/**/*']`)
- `vendored_gems_path` (default: `'vendored/'`)
- `cache_directory` (default: `'tmp/cache/codeowners'`)
- `ignore_dirs` (default includes: `.git`, `node_modules`, `tmp`, etc.)

See examples in `tests/fixtures/**/config/` for reference setups.

## Cache

By default, cache is stored under `tmp/cache/codeowners` relative to the project root. This speeds up repeated runs.

- Disable cache for a run: add the global flag `--no-cache`
- Clear all cache: `codeowners delete-cache`

## Validation

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

1. Only one mechanism defines ownership for any file.
2. All referenced teams are valid.
3. All files in `owned_globs` are owned, unless in `unowned_globs`.
4. The `CODEOWNERS` file is up to date.
3. All files in `owned_globs` are owned, unless matched by `unowned_globs`.
4. The generated `CODEOWNERS` file is up to date.

Exit status is non-zero on errors.

## Library Usage

Import public APIs from `codeowners::runner::*`.

```rust
use codeowners::runner::{RunConfig, for_file, teams_for_files_from_codeowners};

fn main() {
let run_config = RunConfig {
project_root: std::path::PathBuf::from("."),
codeowners_file_path: std::path::PathBuf::from(".github/CODEOWNERS"),
config_path: std::path::PathBuf::from("config/code_ownership.yml"),
no_cache: true, // set false to enable on-disk caching
};

// Find owner for a single file using the optimized path (not just CODEOWNERS)
let result = for_file(&run_config, "app/models/user.rb", false);
for msg in result.info_messages { println!("{}", msg); }
for err in result.io_errors { eprintln!("io: {}", err); }
for err in result.validation_errors { eprintln!("validation: {}", err); }

// Map multiple files to teams using CODEOWNERS rules only
let files = vec![
"app/models/user.rb".to_string(),
"config/teams/payroll.yml".to_string(),
];
match teams_for_files_from_codeowners(&run_config, &files) {
Ok(map) => println!("{:?}", map),
Err(e) => eprintln!("error: {}", e),
}
}
```

## Development

Expand All @@ -190,3 +277,11 @@ codeowners for-team Payroll
```

- Please update `CHANGELOG.md` and this `README.md` when making changes.

### Module layout

- `src/runner.rs`: public façade re-exporting the API and types.
- `src/runner/api.rs`: externally available functions used by the CLI and other crates.
- `src/runner/types.rs`: `RunConfig`, `RunResult`, and runner `Error`.
- `src/ownership/`: all ownership logic (parsing, mapping, validation, generation).
- `src/ownership/codeowners_query.rs`: CODEOWNERS-only queries consumed by the façade.
8 changes: 8 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use serde::Deserialize;
use std::{fs::File, path::Path};

#[derive(Deserialize, Debug, Clone)]
pub struct Config {
Expand Down Expand Up @@ -77,6 +78,13 @@ fn default_ignore_dirs() -> Vec<String> {
]
}

impl Config {
pub fn load_from_path(path: &Path) -> std::result::Result<Self, String> {
let file = File::open(path).map_err(|e| format!("Can't open config file: {} ({})", path.to_string_lossy(), e))?;
serde_yaml::from_reader(file).map_err(|e| format!("Can't parse config file: {} ({})", path.to_string_lossy(), e))
}
}

#[cfg(test)]
mod tests {
use std::{
Expand Down
2 changes: 1 addition & 1 deletion src/crosscheck.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::path::Path;
use crate::{
cache::Cache,
config::Config,
ownership::for_file_fast::find_file_owners,
ownership::file_owner_resolver::find_file_owners,
project::Project,
project_builder::ProjectBuilder,
runner::{RunConfig, RunResult, config_from_path, team_for_file_from_codeowners},
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub(crate) mod common_test;
pub mod config;
pub mod crosscheck;
pub mod ownership;
pub mod path_utils;
pub(crate) mod project;
pub mod project_builder;
pub mod project_file_builder;
Expand Down
3 changes: 2 additions & 1 deletion src/ownership.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ use std::{
use tracing::{info, instrument};

pub(crate) mod codeowners_file_parser;
pub(crate) mod codeowners_query;
mod file_generator;
mod file_owner_finder;
pub mod for_file_fast;
pub mod file_owner_resolver;
pub(crate) mod mapper;
mod validator;

Expand Down
53 changes: 53 additions & 0 deletions src/ownership/codeowners_query.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use std::collections::HashMap;
use std::path::{Path, PathBuf};

use crate::ownership::codeowners_file_parser::Parser;
use crate::project::Team;

pub(crate) fn team_for_file_from_codeowners(
project_root: &Path,
codeowners_file_path: &Path,
team_file_globs: &[String],
file_path: &Path,
) -> Result<Option<Team>, String> {
let relative_file_path = if file_path.is_absolute() {
crate::path_utils::relative_to_buf(project_root, file_path)
} else {
PathBuf::from(file_path)
};

let parser = Parser {
codeowners_file_path: codeowners_file_path.to_path_buf(),
project_root: project_root.to_path_buf(),
team_file_globs: team_file_globs.to_vec(),
};

parser.team_from_file_path(&relative_file_path).map_err(|e| e.to_string())
}

pub(crate) fn teams_for_files_from_codeowners(
project_root: &Path,
codeowners_file_path: &Path,
team_file_globs: &[String],
file_paths: &[String],
) -> Result<HashMap<String, Option<Team>>, String> {
let relative_file_paths: Vec<PathBuf> = file_paths
.iter()
.map(Path::new)
.map(|path| {
if path.is_absolute() {
crate::path_utils::relative_to_buf(project_root, path)
} else {
path.to_path_buf()
}
})
.collect();

let parser = Parser {
codeowners_file_path: codeowners_file_path.to_path_buf(),
project_root: project_root.to_path_buf(),
team_file_globs: team_file_globs.to_vec(),
};

parser.teams_from_files_paths(&relative_file_paths).map_err(|e| e.to_string())
}
Loading