vcs_diff/lib.rs
1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![deny(rustdoc::broken_intra_doc_links)]
3//! `vcs-diff` — the shared git-format unified-diff model and parser for the
4//! [vcs-toolkit-rs](https://github.com/ZelAnton/vcs-toolkit-rs) workspace.
5//!
6//! `git diff` and `jj diff --git` emit the same **git-format unified diffs**
7//! (byte-identical for ASCII paths; they differ only in non-ASCII filename
8//! rendering — git octal-C-quotes by default, jj writes raw UTF-8 — both of which
9//! the parser decodes), so `vcs-git` and `vcs-jj` share one model and one parser
10//! here rather than each carrying a copy that could silently drift. This is the foundational
11//! crate both depend on: **std only**, no async, no subprocess — pure data types
12//! and pure functions over text the wrapper crates obtained elsewhere.
13//!
14//! # The surface
15//!
16//! - **[`parse_diff`]** — the entry point. Turns git-format diff text into one
17//! [`FileDiff`] per file; the same call serves `git diff` and `jj diff --git`
18//! output alike. Pure and total: arbitrary CLI bytes in, never a panic.
19//! - **[`FileDiff`]** — one file's entry: its [`ChangeKind`], the
20//! forward-slash-normalised `path` (and `old_path` for a rename), the
21//! [`Hunk`]s, and the verbatim `raw` section for callers that display text.
22//! - **[`Hunk`]** — a single `@@ … @@` block: the old/new line ranges, the
23//! section heading, and a body of **[`DiffLine`]**s
24//! ([`Context`](DiffLine::Context) / [`Added`](DiffLine::Added) /
25//! [`Removed`](DiffLine::Removed)), each with its leading marker and line
26//! terminator stripped.
27//! - **[`ChangeKind`]** — how the file changed: [`Added`](ChangeKind::Added) /
28//! [`Modified`](ChangeKind::Modified) / [`Deleted`](ChangeKind::Deleted) /
29//! [`Renamed`](ChangeKind::Renamed).
30//! - **[`DiffStat`]** — the aggregate `files_changed`/`insertions`/`deletions`
31//! shape both `git diff --shortstat` and `jj diff --stat` parse into.
32//! - **[`DiffSpec`]** — the diff request a wrapper's `diff`/`diff_text` takes
33//! ([`WorkingTree`](DiffSpec::WorkingTree) / [`Rev`](DiffSpec::Rev)). This
34//! crate defines it (so the backends share one type) but has no method that
35//! consumes it.
36//! - **[`Version`]** + **[`parse_dotted_version`]** — a numeric
37//! `major.minor.patch` (it `Ord`s, so a caller can gate on a minimum) read
38//! tolerantly from a `<tool> --version` banner.
39//!
40//! The wrapper crates re-export these (e.g. `vcs_git::FileDiff`,
41//! `vcs_git::parse_diff`, `vcs_git::GitVersion`), so consumers rarely name this
42//! crate directly.
43//!
44//! # Recipes
45//!
46//! Parse a one-file modify diff and read the structured result — pure, so this
47//! runs as written:
48//!
49//! ```rust
50//! use vcs_diff::{parse_diff, ChangeKind, DiffLine};
51//!
52//! let text = "\
53//! diff --git a/f b/f
54//! --- a/f
55//! +++ b/f
56//! @@ -1,2 +1,2 @@ fn main()
57//! ctx
58//! -old
59//! +new
60//! ";
61//! let files = parse_diff(text);
62//! assert_eq!(files.len(), 1);
63//! assert_eq!(files[0].change, ChangeKind::Modified);
64//! assert_eq!(files[0].path, "f");
65//!
66//! let hunk = &files[0].hunks[0];
67//! assert_eq!((hunk.old_start, hunk.new_start), (1, 1));
68//! assert_eq!(hunk.section, "fn main()");
69//! assert_eq!(hunk.lines, vec![
70//! DiffLine::Context("ctx".into()),
71//! DiffLine::Removed("old".into()),
72//! DiffLine::Added("new".into()),
73//! ]);
74//! ```
75//!
76//! # Features
77//!
78//! - **`serde`** — derives `serde::Serialize` on every model type
79//! ([`FileDiff`], [`Hunk`], [`DiffLine`], [`ChangeKind`], [`DiffStat`],
80//! [`Version`]) so a caller can emit the parsed diff as JSON.
81
82mod diff;
83mod version;
84
85pub use diff::{ChangeKind, DiffLine, DiffSpec, DiffStat, FileDiff, Hunk, parse_diff};
86pub use version::{Version, parse_dotted_version};