Skip to main content

gitkit_cli/git/
kit.rs

1use std::{collections::HashSet, path::Path};
2
3use git2::{Commit, Diff, DiffOptions, Error, Oid, Repository, StatusOptions, Statuses};
4
5use crate::git::{model::KitCommit, status::KitStatus};
6
7pub struct KitRepo {
8    pub inner: Repository,
9}
10
11impl<'repo> KitRepo {
12    pub fn open<P: AsRef<Path>>(path: P) -> Result<KitRepo, Error> {
13        let repo = Repository::open(path)?;
14        Ok(KitRepo { inner: repo })
15    }
16
17    pub fn change_branch(&self, branch_name: &str) -> Result<(), git2::Error> {
18        let branch = self
19            .inner
20            .find_branch(branch_name, git2::BranchType::Local)?;
21        let reference = branch.get();
22
23        let obj = reference.peel_to_commit()?;
24        self.inner.set_head(reference.name().unwrap())?;
25
26        self.inner.checkout_tree(obj.as_object(), None)?;
27
28        Ok(())
29    }
30
31    pub fn list_branch(&self) -> Result<(), git2::Error> {
32        let branches = self.inner.branches(Some(git2::BranchType::Local))?;
33        branches.filter_map(|x| x.ok()).for_each(|y| {
34            let name =
35                y.0.name()
36                    .unwrap_or(Some("No name found"))
37                    .unwrap_or_default();
38
39            println!("{}", name);
40        });
41        Ok(())
42    }
43
44    // use iter_commits unless Vec<Commit> is needed
45    pub fn get_all_commits<'a>(&'a self) -> Result<Vec<KitCommit>, Error> {
46        let revwalk: Vec<Oid> = match self.inner.revwalk() {
47            Ok(mut walk) => {
48                if let Err(e) = walk.push_head() {
49                    panic!("Failed to push HEAD to revwalk: {}", e);
50                }
51
52                walk.into_iter().flatten().collect()
53            }
54            Err(e) => panic!("Failed to create revwalk: {}", e),
55        };
56
57        let commits: Vec<KitCommit> = revwalk
58            .iter()
59            .map(|oid| self.inner.find_commit(*oid).unwrap())
60            .map(|c| KitCommit::from_git2(&c))
61            .collect();
62
63        Ok(commits)
64    }
65
66    pub fn iter_commits(&self) -> Result<impl Iterator<Item = KitCommit>, git2::Error> {
67        let mut revwalk = self.inner.revwalk()?;
68        revwalk.push_head()?;
69
70        let repo_ref = &self.inner;
71
72        Ok(revwalk
73            .flatten()
74            .filter_map(move |oid| repo_ref.find_commit(oid).ok())
75            .map(|c| KitCommit::from_git2(&c)))
76    }
77
78    pub fn get_authors(&self) -> Result<HashSet<String>, git2::Error> {
79        let mut authors: HashSet<String> = HashSet::new();
80        for commit in self.iter_commits()? {
81            authors.insert(commit.email);
82        }
83
84        Ok(authors)
85    }
86
87    pub fn get_author_commits(
88        &self,
89        email: &str,
90    ) -> Result<impl Iterator<Item = KitCommit>, git2::Error> {
91        let email_owned = email.to_string();
92        let iter = self
93            .iter_commits()?
94            .filter(move |commit| commit.email == email_owned);
95
96        Ok(iter)
97    }
98
99    pub fn get_diff(
100        &self,
101        parent: Option<&Commit>,
102        current: Option<&Commit>,
103        opts: Option<&mut DiffOptions>,
104    ) -> Result<Diff<'_>, git2::Error> {
105        let parent_tree = parent.map(|c| c.tree()).transpose()?;
106        let current_tree = current.map(|c| c.tree()).transpose()?;
107
108        let diff =
109            self.inner
110                .diff_tree_to_tree(parent_tree.as_ref(), current_tree.as_ref(), opts)?;
111        Ok(diff)
112    }
113
114    pub fn get_parent_diff(
115        &self,
116        commit: &Commit<'repo>,
117        opts: Option<&mut DiffOptions>,
118    ) -> Result<Diff<'_>, git2::Error> {
119        let parent_commit = match commit.parent(0) {
120            Ok(parent) => Some(parent),
121            Err(_) => None,
122        };
123
124        self.get_diff(parent_commit.as_ref(), Some(commit), opts)
125    }
126
127    // get all diffs exlcuding merge commits
128    pub fn iter_diff_history<'a>(
129        &'a self,
130    ) -> Result<impl Iterator<Item = (KitCommit, Diff<'a>)> + 'a, git2::Error> {
131        let mut revwalk = self.inner.revwalk()?;
132        revwalk.push_head()?;
133
134        let repo = &self.inner;
135
136        let diff_iter = revwalk.filter_map(move |oid_result| {
137            let oid = oid_result.ok()?;
138            let commit = repo.find_commit(oid).ok()?;
139
140            // Skip merge commits
141            if commit.parent_count() > 1 {
142                return None;
143            }
144
145            let commit_tree = commit.tree().ok()?;
146
147            let parent_tree = if commit.parent_count() == 1 {
148                commit.parent(0).ok()?.tree().ok()
149            } else {
150                None
151            };
152
153            let mut diff_options = DiffOptions::new();
154            diff_options.ignore_whitespace(true);
155
156            let diff = repo
157                .diff_tree_to_tree(
158                    parent_tree.as_ref(),
159                    Some(&commit_tree),
160                    Some(&mut diff_options),
161                )
162                .ok()?;
163
164            Some((KitCommit::from_git2(&commit), diff))
165        });
166
167        Ok(diff_iter)
168    }
169
170    pub fn current_branch(&self) -> Result<String, git2::Error> {
171        let head = self.inner.head()?;
172        head.shorthand().map(|s| s.to_owned())
173    }
174
175    pub fn get_status(&self) -> KitStatus {
176        KitStatus::new(&self)
177    }
178
179    fn get_raw_status(&self) -> Result<Statuses<'_>, git2::Error> {
180        let mut opts = StatusOptions::new();
181        opts.include_untracked(true)
182            .recurse_untracked_dirs(true)
183            .include_ignored(false);
184
185        let statuses = self.inner.statuses(Some(&mut opts))?;
186
187        Ok(statuses)
188    }
189
190    pub fn is_dirty(&self) -> Result<bool, git2::Error> {
191        let statuses = self.get_raw_status()?;
192        Ok(!statuses.is_empty())
193    }
194}