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 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 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 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}