Diff OVERview: An Experimental Supplement to Git Diffs
I've been thinking about version control and how it could be more effective. I've used Git and centralized repositories like GitHub and Bitbucket, for most of my programming life. In general, it has served me well enough as a foundational version control tool. My gripe is the general lack of additional tooling built on top of it, particularly for viewing and understanding changes in their semantic context.
The problem with diffs as presented by most diffing tools (e.g. GitHub's PR interface) is that they're too raw, too detached from the system I'm building. They're just a cleaned up presentation of what is still essentially a textual diff.
When I read a diff, I spend significant time putting the textual changes in the context of the system before mentally comparing the system's behavior between versions. In some cases, especially when reviewing a large set of changes (of which I've been on both the giving and receiving ends 😅), I've felt like I wanted to see an overview of high-level changes, before going into the full diff. My reasoning is that if I can establish the context of the changes more quickly, I can get to the heart of a PR faster while eliminating some cruft around my current workflow.
I recently decided to test that theory and ended up with a tool called dover ("Diff OVERview"). The rest of this post will explain what it is, how it works, and plans for upcoming improvements.
dover
dover is a CLI tool that provides a semantic, high-level overview of Git-based diffs for Rust code. It has two major dependencies: libgit2 for interacting with Git repositories and syn for working with Rust code.
When run from the command line (like dover diff [commit1 [commit2]]), dover will:
- Collect the set of changes between the two commits with
libgit2, - Filter out any non-Rust source files,
- Parse the files with
synto get ASTs, - Convert the ASTs into an "overview" AST containing only the things we want to diff,
- Diff the overviews of each file,
- Output the results.
The crux of the project is really the overview AST: an opinionated subset of syntactic elements that we want to compare.
For each file in the git diff, dover generates a top-level Overview that looks like this:
#[derive(Debug)]
pub struct Overview {
path: PathBuf,
uses: Uses,
structs: Structs,
enums: Enums,
traits: Traits,
functions: Functions,
impls: Impls,
}
Each overview element, except path, is itself a subset of its full AST so, for example, a standalone function looks like this:
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Function {
vis: Visibility,
r#const: Option<Const>,
r#async: Option<Async>,
r#unsafe: Option<Unsafe>,
abi: Option<Abi>,
name: String,
generics: Generics,
inputs: Inputs,
output: ReturnType,
original_fn: syn::ItemFn,
source: SourceFile,
}
All elements originate from their counterparts in syn but with "extra" information
removed. For example, an "overview" of a function does not include the function body.
Sample Diff
Suppose we have these two arbitrary, made-up sample files:
// old somefile.rs
use std::io::{Read, Write};
use tokio::net::TcpListener;
enum ApplicationProtocol<T: Clone> {
Http { t: T },
Https,
Stun,
Smtp,
}
pub fn build<T, U, V>(i: u32, y: T) -> T
where
U: Read,
V: Write,
{
i
}
struct RandomStruct {
field1: u8,
field2: u16,
field3: u32,
}
impl RandomStruct {
fn new(field1: u8, field2: u16, field3: u32) -> Self {
Self {
field1,
field2,
field3,
}
}
fn do_it(&self, x: String) {
println!("do_it called with x: {}", x);
}
}
// new - somefile.rs
use std::io::{Read, Seek, Write};
use tokio::net::TcpStream;
async fn build<T, U, V>(x: String, y: T) -> u32
where
U: Read,
V: Write + Send,
{
todo!()
}
enum ApplicationProtocol<T: Clone + Send, U> {
Dns,
Http { t: T, u: U },
Https { t: T },
}
struct RandomStruct {
field1: u8,
field3: u32,
}
impl RandomStruct {
fn new(field1: u8, field3: u32) -> Self {
Self { field1, field3 }
}
pub fn do_it(&self, x: String, y: String) {
println!("do_it called with ({x}, {y})");
}
pub fn do_it_again(&self) -> Result<(), String> {
println!("I'm doing it again!");
Ok(())
}
}
Notice that in the second file, certain fields and variants were added or removed, interfaces were changed, and some items were reordered.
A dover diff of those files to stdout looks like this: 
If we use the --to-html flag when diffing, dover generates a static webpage showing the
same contents: 
There's still plenty of room for polish and bugfixes, but the general idea has mostly taken shape. A few things dover does include:
- showing diffed elements based on their semantic parts, irrespective of formatting,
- flattening
usestatements to explicitly show import changes, - eliding function bodies, irrelevant struct and enum variant fields.
- diffing elements irrespective of their order of occurrence in a file, unless order has meaning (like in the case of function args or tuple struct fields).
One key point is that the diff is opinionated, and not everyone agrees on what is and
isn't relevant to an "overview" diff. After finishing the remaining syntactic elements
(check the roadmap), the
last thing I'd like to add is a dover.toml file for customizing what's included in the
diff. I'd like customization to include what syntactic elements to diff, as well as the
order of appearance of the major "groups" (structs, traits, etc.).
Is it Useful?
I've used dover when reviewing large diffs and found it works mostly as intended, especially when trying to filter out what a PR is not (like when someone changes a small set of interfaces and the diff shows 30+ files changed, but it's mostly signature-related fallout).
I created the HTML output with the vision of integrating it into GitHub PRs, so that whenever someone opens a PR, a GitHub workflow will run dover and paste a link to the HTML diff. Integrating with our existing workflows while running automatically will let me see if the idea has actual long-term merit, or if it's just a neat trick. Time will tell!
Conclusion
dover is available on crates.io/crates/dover if you want
to try it out: cargo install dover.
If you use it and would like to leave feedback or suggestions, feel free to contact me on GitHub! It'd be great to hear other thoughts on dover, diffs, development tools, or the state of programming in general.
Thanks for reading!