1use bstr::ByteSlice;
2
3#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
4pub(crate) struct Branch {
5 pub(crate) name: String,
6 pub(crate) id: git2::Oid,
7 pub(crate) push_id: Option<git2::Oid>,
8 pub(crate) pull_id: Option<git2::Oid>,
9}
10
11#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub(crate) struct Commit {
13 pub(crate) id: git2::Oid,
14 pub(crate) tree_id: git2::Oid,
15 pub(crate) summary: bstr::BString,
16 pub(crate) time: std::time::SystemTime,
17 pub(crate) author: Option<std::rc::Rc<str>>,
18 pub(crate) committer: Option<std::rc::Rc<str>>,
19}
20
21pub struct GitRepo {
22 repo: git2::Repository,
23 push_remote: Option<String>,
24 pull_remote: Option<String>,
25 commits: std::cell::RefCell<std::collections::HashMap<git2::Oid, std::rc::Rc<Commit>>>,
26 interned_strings: std::cell::RefCell<std::collections::HashSet<std::rc::Rc<str>>>,
27}
28
29impl GitRepo {
30 pub fn new(repo: git2::Repository) -> Self {
31 Self {
32 repo,
33 push_remote: None,
34 pull_remote: None,
35 commits: Default::default(),
36 interned_strings: Default::default(),
37 }
38 }
39
40 pub(crate) fn push_remote(&self) -> &str {
41 self.push_remote.as_deref().unwrap_or("origin")
42 }
43
44 pub(crate) fn pull_remote(&self) -> &str {
45 self.pull_remote.as_deref().unwrap_or("origin")
46 }
47
48 pub fn raw(&self) -> &git2::Repository {
49 &self.repo
50 }
51
52 pub fn raw_mut(&mut self) -> &mut git2::Repository {
53 &mut self.repo
54 }
55
56 pub(crate) fn find_commit(&self, id: git2::Oid) -> Option<std::rc::Rc<Commit>> {
57 let mut commits = self.commits.borrow_mut();
58 if let Some(commit) = commits.get(&id) {
59 Some(std::rc::Rc::clone(commit))
60 } else {
61 let commit = self.repo.find_commit(id).ok()?;
62 let summary: bstr::BString = commit.summary_bytes().unwrap().into();
63 let time = std::time::SystemTime::UNIX_EPOCH
64 + std::time::Duration::from_secs(commit.time().seconds().max(0) as u64);
65
66 let author = commit.author().name().map(|n| self.intern_string(n));
67 let committer = commit.author().name().map(|n| self.intern_string(n));
68 let commit = std::rc::Rc::new(Commit {
69 id: commit.id(),
70 tree_id: commit.tree_id(),
71 summary,
72 time,
73 author,
74 committer,
75 });
76 commits.insert(id, std::rc::Rc::clone(&commit));
77 Some(commit)
78 }
79 }
80
81 pub(crate) fn head_branch(&self) -> Option<Branch> {
82 let resolved = self.repo.head().unwrap().resolve().unwrap();
83 let name = resolved.shorthand()?;
84 let id = resolved.target()?;
85
86 let push_id = self
87 .repo
88 .find_branch(
89 &format!("{}/{}", self.push_remote(), name),
90 git2::BranchType::Remote,
91 )
92 .ok()
93 .and_then(|b| b.get().target());
94 let pull_id = self
95 .repo
96 .find_branch(
97 &format!("{}/{}", self.pull_remote(), name),
98 git2::BranchType::Remote,
99 )
100 .ok()
101 .and_then(|b| b.get().target());
102
103 Some(Branch {
104 name: name.to_owned(),
105 id,
106 push_id,
107 pull_id,
108 })
109 }
110
111 pub(crate) fn branch(&mut self, name: &str, id: git2::Oid) -> Result<(), git2::Error> {
112 let commit = self.repo.find_commit(id)?;
113 self.repo.branch(name, &commit, true)?;
114 Ok(())
115 }
116
117 pub(crate) fn find_local_branch(&self, name: &str) -> Option<Branch> {
118 let branch = self.repo.find_branch(name, git2::BranchType::Local).ok()?;
119 let id = branch.get().target().unwrap();
120
121 let push_id = self
122 .repo
123 .find_branch(
124 &format!("{}/{}", self.push_remote(), name),
125 git2::BranchType::Remote,
126 )
127 .ok()
128 .and_then(|b| b.get().target());
129 let pull_id = self
130 .repo
131 .find_branch(
132 &format!("{}/{}", self.pull_remote(), name),
133 git2::BranchType::Remote,
134 )
135 .ok()
136 .and_then(|b| b.get().target());
137
138 Some(Branch {
139 name: name.to_owned(),
140 id,
141 push_id,
142 pull_id,
143 })
144 }
145
146 pub(crate) fn local_branches(&self) -> impl Iterator<Item = Branch> + '_ {
147 log::trace!("Loading branches");
148 self.repo
149 .branches(Some(git2::BranchType::Local))
150 .into_iter()
151 .flatten()
152 .flat_map(move |branch| {
153 let (branch, _) = branch.ok()?;
154 let name = if let Some(name) = branch.name().ok().flatten() {
155 name
156 } else {
157 log::debug!(
158 "Ignoring non-UTF8 branch {:?}",
159 branch.name_bytes().unwrap().as_bstr()
160 );
161 return None;
162 };
163 let id = branch.get().target().unwrap();
164
165 let push_id = self
166 .repo
167 .find_branch(
168 &format!("{}/{}", self.push_remote(), name),
169 git2::BranchType::Remote,
170 )
171 .ok()
172 .and_then(|b| b.get().target());
173 let pull_id = self
174 .repo
175 .find_branch(
176 &format!("{}/{}", self.pull_remote(), name),
177 git2::BranchType::Remote,
178 )
179 .ok()
180 .and_then(|b| b.get().target());
181
182 Some(Branch {
183 name: name.to_owned(),
184 id,
185 push_id,
186 pull_id,
187 })
188 })
189 }
190
191 pub(crate) fn detach(&mut self) -> Result<(), git2::Error> {
192 let head_id = self
193 .repo
194 .head()
195 .unwrap()
196 .resolve()
197 .unwrap()
198 .target()
199 .unwrap();
200 self.repo.set_head_detached(head_id)?;
201 Ok(())
202 }
203
204 pub(crate) fn switch(&mut self, name: &str) -> Result<(), git2::Error> {
205 let branch = self.repo.find_branch(name, git2::BranchType::Local)?;
207 self.repo.set_head(branch.get().name().unwrap())?;
208 let mut builder = git2::build::CheckoutBuilder::new();
209 builder.force();
210 self.repo.checkout_head(Some(&mut builder))?;
211 Ok(())
212 }
213
214 fn intern_string(&self, data: &str) -> std::rc::Rc<str> {
215 let mut interned_strings = self.interned_strings.borrow_mut();
216 if let Some(interned) = interned_strings.get(data) {
217 std::rc::Rc::clone(interned)
218 } else {
219 let interned = std::rc::Rc::from(data);
220 interned_strings.insert(std::rc::Rc::clone(&interned));
221 interned
222 }
223 }
224}
225
226impl std::fmt::Debug for GitRepo {
227 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
228 f.debug_struct("GitRepo")
229 .field("repo", &self.repo.workdir())
230 .field("push_remote", &self.push_remote.as_deref())
231 .field("pull_remote", &self.pull_remote.as_deref())
232 .finish()
233 }
234}