rpo 0.1.0-beta.5

Git contribution analysis: commits, file changes, and per-line authorship over time as polars DataFrames
# rpo

Git-contribution analysis. Walks a repository and produces tabular data
(via [polars]) about commits, file changes, and per-line authorship over
time — who wrote what, when, and how much of it survives today.

For the command-line tool and local web UI built on this crate, see
[`rpo-cli`]. For Python bindings, see [`rpo` on PyPI].

```rust,no_run
use rpo::RepoAnalyzer;

let analysis = RepoAnalyzer::open(".")?
    .with_exclude_globs(vec!["**/*.lock".to_string()])
    .all()?;

println!("{} commits", analysis.commits.height());
println!("{} file changes", analysis.file_changes.height());
# Ok::<(), rpo::RpoError>(())
```

## How it works

[`RepoAnalyzer::open`] returns a [`Builder`]. Configure the walk with the
`with_*` methods, then call a terminal to get DataFrames back:

| Terminal | Returns |
|---|---|
| [`commits`] | one row per commit |
| [`file_changes`] | one row per (commit, file) touched |
| [`blame`] | per-line authorship at HEAD |
| [`blame_over_time`] | per-line authorship at each snapshot |
| [`all`] | every frame, from a single walk |

Prefer `all()` when you need more than one frame — it walks once.

```rust,no_run
use rpo::{RepoAnalyzer, SnapshotSelector};

// Only the source tree, first-parent history, monthly blame snapshots.
let timeline = RepoAnalyzer::open(".")?
    .with_include_globs(vec!["src/**".to_string()])
    .with_blame_snapshots(SnapshotSelector::Monthly)
    .blame_over_time()?;
# Ok::<(), rpo::RpoError>(())
```

## Reports

[`rpo::reports`] has ready-made transforms over those frames — summary,
per-author and per-file activity, and blame timelines. [`rpo::bus_factor`]
computes knowledge concentration three ways (threshold, ABF, and JBF).

```rust,no_run
use rpo::{RepoAnalyzer, options::Identity};

let analysis = RepoAnalyzer::open(".")?.all()?;
let report = rpo::reports::author_report(
    &analysis.commits,
    &analysis.file_changes,
    Identity::default(),
    Default::default(),
    Default::default(),
)?;
# Ok::<(), rpo::RpoError>(())
```

They are ordinary polars, so writing your own is expected — the frames
are the real API.

## Conventions

Two things to know before reading a frame:

**Identities are canonicalized.** `.mailmap` is applied automatically,
and the result lands in `canonical_author_name`,
`canonical_committer_email`, and so on. Group on those, not the raw
`author_name` — otherwise one person committing under several addresses
counts as several people. [`IdentityMap`] adds your own aliases on top.

**Datetimes are UTC milliseconds.** Every time column is
`Datetime(TimeUnit::Milliseconds)` with no timezone.

Columns, by frame:

- **commits**`sha`, `short_sha`, `author_name`, `author_email`,
  `committer_name`, `committer_email`, `canonical_*`, `author_time`,
  `commit_time`, `parent_count`, `is_merge`, `message_subject`,
  `files_changed`, `insertions`, `deletions`
- **file_changes**`sha`, `commit_time`, `canonical_*`, `path`,
  `old_path`, `change_kind`, `insertions`, `deletions`, `extension`,
  `is_generated`, `is_vendored`
- **blame**`snapshot_sha`, `snapshot_time`, `snapshot_label`, `path`,
  `start_line`, `line_count`, `commit_sha`, `commit_time`,
  `canonical_*`, `extension`, `is_generated`, `is_vendored`

Path filters ([`with_include_globs`], [`with_exclude_globs`]) are applied
during the walk, so excluded paths never enter any frame and cost nothing
downstream. Patterns are [globset] syntax matched against the full
repo-relative path; separators are not special, so `*.rs` matches
`src/web/pages.rs`. Exclusion wins over inclusion.

## Streaming

Repositories large enough that the blame timeline will not fit in memory
can stream it instead, one snapshot at a time, via
[`blame_over_time_streaming`] and a [`FrameSink`] — [`ParquetSink`],
[`ParquetDirSink`], or `DuckDbSink`.

## Features

| Feature | Default | Description |
|---|---|---|
| `backend-gix` | **on** | Git access via [gix] |
| `backend-git2` | off | [git2] backend — a compile-time seam only; every method is `todo!()` |
| `sink-duckdb` | off | `DuckDbSink`, for streaming blame into DuckDB |

Exactly one backend must be enabled, or the crate fails to compile.

`sink-duckdb` is off by default because duckdb is statically bundled and
compiles a large C++ amalgamation, roughly doubling a clean build.

```toml
# with the DuckDB sink
rpo = { version = "0.1", features = ["sink-duckdb"] }
```

## Compatibility

Pre-1.0: minor releases may break the API. `0.1.0-beta.5` renamed the
options types for clarity — `Aggregate` is now [`Role`], `Identify` is
[`IdentityField`], `Aggregation` is [`Identity`] (with fields `role` and
`field`), and `ActivityOptions` is [`Activity`].

[`Role`]: options::Role
[`IdentityField`]: options::IdentityField
[`Identity`]: options::Identity
[`Activity`]: options::Activity

## Minimum supported Rust version

1.95.

[polars]: https://pola.rs
[gix]: https://github.com/Byron/gitoxide
[git2]: https://github.com/rust-lang/git2-rs
[globset]: https://docs.rs/globset
[`rpo-cli`]: https://crates.io/crates/rpo-cli
[`rpo` on PyPI]: https://pypi.org/project/rpo/