summary refs log tree commit diff
path: root/src/main.rs
blob: d5d763d35a0dcb9f9c5e914401314435d499545b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021 Alyssa Ross <hi@alyssa.is>

mod diff_thread;
mod filter;

use diff_thread::{DiffThread, FileDiff, MakeDiff};
use filter::Filter;

use std::env::args_os;
use std::ffi::OsString;
use std::io::{stderr, Read, Write};
use std::os::unix::prelude::*;
use std::process::{exit, ChildStdout};
use std::thread::spawn;

use git2::{Delta, Diff, Oid, Repository, Sort, Tree};

fn child_out_bytes(child: &mut ChildStdout) -> impl Iterator<Item = u8> + '_ {
    child.by_ref().bytes().map(Result::unwrap)
}

fn output_eq(lhs: &mut ChildStdout, rhs: &mut ChildStdout) -> bool {
    child_out_bytes(lhs).eq(child_out_bytes(rhs))
}

fn filtered_file_same(
    repo: &Repository,
    old: &Tree,
    new: &Tree,
    diff: FileDiff,
    argv: &[OsString],
) -> bool {
    if diff.status != Delta::Modified {
        return false;
    }

    let path = diff.old_path.unwrap();

    let (filter_old, mut stdout_old) = Filter::new(&repo, &old, &path, argv);
    let (filter_new, mut stdout_new) = Filter::new(&repo, &new, &path, argv);

    let thread = spawn(move || output_eq(&mut stdout_old, &mut stdout_new));

    filter_old.wait().expect("TODO: HANDLE FILTER ERROR");
    filter_new.wait().expect("TODO: HANDLE FILTER ERROR");

    thread.join().unwrap()
}

fn open_repo() -> Repository {
    match Repository::open_from_env() {
        Ok(repo) => repo,
        Err(e) => {
            eprintln!("fatal: {}", e.message());
            exit(128);
        }
    }
}

struct DiffMaker {
    old_tree_oid: Oid,
    new_tree_oid: Oid,
}

impl MakeDiff for DiffMaker {
    fn make_diff(self, repo: &Repository) -> Result<Diff, git2::Error> {
        let old_tree = repo.find_tree(self.old_tree_oid)?;
        let new_tree = repo.find_tree(self.new_tree_oid)?;
        repo.diff_tree_to_tree(Some(&old_tree), Some(&new_tree), Default::default())
    }
}

fn main() {
    let args: Vec<_> = args_os().collect();
    if args.len() < 2 {
        eprintln!("fatal: no command given");
        exit(128);
    }

    if let Some(arg) = args
        .iter()
        .skip(1)
        .take_while(|a| a.as_bytes().starts_with(b"-"))
        .next()
    {
        eprint!("fatal: unrecognized argument: ");
        let _ = stderr().write_all(arg.as_bytes());
        eprintln!();
        exit(128);
    }

    let repo = open_repo();

    let diff_thread =
        DiffThread::new(Repository::open(repo.path()).expect("opening diff thread repo"));

    let mut walk = repo.revwalk().expect("revwalk");
    walk.set_sorting(Sort::TOPOLOGICAL).expect("set_sorting");
    walk.push_head().expect("push_head");

    // It might be good to parallelize this loop at some point, but
    // we'd want to preserve order.
    for commit_oid in walk {
        let commit_oid = commit_oid.expect("revwalk");
        let commit = repo.find_commit(commit_oid).expect("find_commit");

        // eprint!("checking {:.7} ", commit_oid);
        // let _ = stderr().write_all(commit.message_bytes().split(|b| *b == b'\n').next().unwrap());
        // eprintln!();

        if commit.parent_count() != 1 {
            continue;
        }

        // TODO: typechange
        // TODO: filemode
        // TODO: submodules
        //
        // TODO: fix all these unwraps

        let parent_tree = commit.parents().next().unwrap().tree().unwrap();
        let commit_tree = commit.tree().unwrap();

        if diff_thread
            .diff_files(DiffMaker {
                old_tree_oid: parent_tree.id(),
                new_tree_oid: commit_tree.id(),
            })
            .all(|diff| {
                filtered_file_same(&repo, &parent_tree, &commit_tree, diff.unwrap(), &args[1..])
            })
        {
            println!("{}", commit.id());
        }
    }
}