Skip to main content

gitcortex_indexer/
differ.rs

1use std::path::{Path, PathBuf};
2
3use git2::{Delta, DiffOptions, Repository};
4use gitcortex_core::error::{GitCortexError, Result};
5
6// ── Types ─────────────────────────────────────────────────────────────────────
7
8/// What happened to a file between two commits.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum FileChange {
11    Added(PathBuf),
12    Modified(PathBuf),
13    Deleted(PathBuf),
14}
15
16impl FileChange {
17    pub fn path(&self) -> &Path {
18        match self {
19            FileChange::Added(p) | FileChange::Modified(p) | FileChange::Deleted(p) => p,
20        }
21    }
22}
23
24// ── Differ ────────────────────────────────────────────────────────────────────
25
26/// Wraps a `git2::Repository` and computes file-level change sets between commits.
27pub struct Differ {
28    repo: Repository,
29}
30
31impl Differ {
32    /// Open the repository at `repo_path` (or any parent that is a git repo).
33    pub fn open(repo_path: &Path) -> Result<Self> {
34        let repo = Repository::discover(repo_path)
35            .map_err(|e| GitCortexError::Git(e.message().to_owned()))?;
36        Ok(Self { repo })
37    }
38
39    /// Hex SHA of the current HEAD commit.
40    pub fn head_sha(&self) -> Result<String> {
41        let head = self
42            .repo
43            .head()
44            .map_err(|e| GitCortexError::Git(e.message().to_owned()))?;
45        let commit = head
46            .peel_to_commit()
47            .map_err(|e| GitCortexError::Git(e.message().to_owned()))?;
48        Ok(commit.id().to_string())
49    }
50
51    /// Compute which files changed between `from_sha` (exclusive) and HEAD.
52    ///
53    /// - `from_sha = None` — diff the empty tree against HEAD (first-time index).
54    /// - `from_sha = Some(sha)` — diff that commit against HEAD.
55    ///
56    /// Only files whose extension is in `supported_exts` are returned.
57    pub fn changed_files(
58        &self,
59        from_sha: Option<&str>,
60        supported_exts: &[&str],
61    ) -> Result<Vec<FileChange>> {
62        let head_tree = self
63            .repo
64            .head()
65            .and_then(|h| h.peel_to_commit())
66            .and_then(|c| c.tree())
67            .map_err(|e| GitCortexError::Git(e.message().to_owned()))?;
68
69        let from_tree = match from_sha {
70            None => None,
71            Some(sha) => {
72                let oid = git2::Oid::from_str(sha)
73                    .map_err(|e| GitCortexError::Git(e.message().to_owned()))?;
74                let commit = self
75                    .repo
76                    .find_commit(oid)
77                    .map_err(|e| GitCortexError::Git(e.message().to_owned()))?;
78                let tree = commit
79                    .tree()
80                    .map_err(|e| GitCortexError::Git(e.message().to_owned()))?;
81                Some(tree)
82            }
83        };
84
85        let mut opts = DiffOptions::new();
86        opts.ignore_whitespace(false);
87
88        let diff = self
89            .repo
90            .diff_tree_to_tree(from_tree.as_ref(), Some(&head_tree), Some(&mut opts))
91            .map_err(|e| GitCortexError::Git(e.message().to_owned()))?;
92
93        let mut changes: Vec<FileChange> = Vec::new();
94
95        diff.foreach(
96            &mut |delta, _progress| {
97                let change = match delta.status() {
98                    Delta::Added | Delta::Copied | Delta::Renamed => delta
99                        .new_file()
100                        .path()
101                        .map(|p| FileChange::Added(p.to_owned())),
102                    Delta::Modified => delta
103                        .new_file()
104                        .path()
105                        .map(|p| FileChange::Modified(p.to_owned())),
106                    Delta::Deleted => delta
107                        .old_file()
108                        .path()
109                        .map(|p| FileChange::Deleted(p.to_owned())),
110                    _ => None,
111                };
112
113                if let Some(c) = change {
114                    let ext = c.path().extension().and_then(|e| e.to_str());
115                    if ext.map(|e| supported_exts.contains(&e)).unwrap_or(false) {
116                        changes.push(c);
117                    }
118                }
119                true
120            },
121            None,
122            None,
123            None,
124        )
125        .map_err(|e| GitCortexError::Git(e.message().to_owned()))?;
126
127        Ok(changes)
128    }
129}