1use anyhow::{Context, Result};
7use chrono::{DateTime, FixedOffset, TimeZone};
8use gix::ObjectId;
9use rust_i18n::t;
10use std::collections::HashSet;
11use std::path::Path;
12
13#[derive(Debug, Clone)]
15pub struct CommitInfo {
16 pub sha: String,
17 pub short_sha: String,
18 pub message: String,
19 pub when: DateTime<FixedOffset>,
20 pub parent_count: usize,
21 pub parents: Vec<String>,
23}
24
25#[derive(Debug, Clone)]
27pub struct TagInfo {
28 pub name: String,
29 pub target_sha: String,
31 pub when: DateTime<FixedOffset>,
32}
33
34pub struct GitRepo {
36 repo: gix::Repository,
37}
38
39fn gix_time_to_chrono(t: gix::date::Time) -> DateTime<FixedOffset> {
40 let offset =
41 FixedOffset::east_opt(t.offset).unwrap_or_else(|| FixedOffset::east_opt(0).unwrap());
42 offset
43 .timestamp_opt(t.seconds, 0)
44 .single()
45 .unwrap_or_else(|| offset.timestamp_opt(0, 0).unwrap())
46}
47
48impl GitRepo {
49 pub fn discover(path: &Path) -> Result<Self> {
51 let repo =
52 gix::discover(path).with_context(|| t!("git.repo_not_found", path = path.display()))?;
53 Ok(Self { repo })
54 }
55
56 pub fn workdir(&self) -> Option<&Path> {
58 self.repo.workdir()
59 }
60
61 pub fn git_dir(&self) -> &Path {
63 self.repo.git_dir()
64 }
65
66 pub fn head_ref_name(&self) -> String {
68 match self.repo.head_name() {
69 Ok(Some(name)) => name.as_bstr().to_string(),
70 _ => self
71 .head_commit()
72 .map(|c| c.short_sha)
73 .unwrap_or_else(|_| "HEAD".into()),
74 }
75 }
76
77 pub fn refs_snapshot(&self) -> Result<Vec<String>> {
79 let mut out = Vec::new();
80 if let Ok(platform) = self.repo.references() {
81 if let Ok(iter) = platform.all() {
82 for reference in iter.flatten() {
83 let name = reference.name().as_bstr().to_string();
84 let target = reference
85 .clone()
86 .into_fully_peeled_id()
87 .map(|id| id.to_string())
88 .unwrap_or_default();
89 out.push(format!("{name} {target}"));
90 }
91 }
92 }
93 out.sort();
94 Ok(out)
95 }
96
97 fn commit_info(commit: &gix::Commit<'_>) -> Result<CommitInfo> {
98 let sha = commit.id().to_string();
99 let when = gix_time_to_chrono(commit.time()?);
100 let message = commit
101 .message_raw()
102 .map(|m| m.to_string())
103 .unwrap_or_default();
104 let parents: Vec<String> = commit.parent_ids().map(|id| id.to_string()).collect();
105 Ok(CommitInfo {
106 short_sha: sha[..7.min(sha.len())].to_string(),
107 sha,
108 message,
109 when,
110 parent_count: parents.len(),
111 parents,
112 })
113 }
114
115 pub fn head_commit(&self) -> Result<CommitInfo> {
117 let commit = self
118 .repo
119 .head_commit()
120 .with_context(|| t!("git.head_read").to_string())?;
121 Self::commit_info(&commit)
122 }
123
124 pub fn current_branch_name(&self) -> Result<String> {
133 if let Some(name) = self.repo.head_name()? {
134 Ok(name.shorten().to_string())
135 } else {
136 let head_sha = self.repo.head_commit()?.id().to_string();
137 let containing = self.branches_containing(&head_sha);
138 if containing.len() == 1 {
139 Ok(containing.into_iter().next().unwrap())
140 } else {
141 Ok("(no branch)".to_string())
142 }
143 }
144 }
145
146 fn local_branches_at(&self, sha: &str) -> Vec<String> {
148 let mut out = Vec::new();
149 if let Ok(platform) = self.repo.references() {
150 if let Ok(branches) = platform.local_branches() {
151 for reference in branches.flatten() {
152 if let Ok(id) = reference.clone().into_fully_peeled_id() {
153 if id.to_string() == sha {
154 out.push(reference.name().shorten().to_string());
155 }
156 }
157 }
158 }
159 }
160 out
161 }
162
163 fn branches_containing(&self, head_sha: &str) -> Vec<String> {
167 let direct = self.local_branches_at(head_sha);
168 if !direct.is_empty() {
169 return direct;
170 }
171 let mut out = Vec::new();
172 if let Ok(platform) = self.repo.references() {
173 if let Ok(branches) = platform.local_branches() {
174 for reference in branches.flatten() {
175 if let Ok(id) = reference.clone().into_fully_peeled_id() {
176 let tip = id.to_string();
177 if self.is_ancestor_of(head_sha, &tip).unwrap_or(false) {
179 out.push(reference.name().shorten().to_string());
180 }
181 }
182 }
183 }
184 }
185 out
186 }
187
188 fn resolve(&self, spec: &str) -> Option<ObjectId> {
190 let id = self.repo.rev_parse_single(spec).ok()?;
191 let commit = id.object().ok()?.try_into_commit().ok()?;
192 Some(commit.id)
193 }
194
195 pub fn tags(&self) -> Result<Vec<TagInfo>> {
197 let mut out = Vec::new();
198 let platform = self.repo.references()?;
199 for reference in platform.tags()?.flatten() {
200 let name = reference.name().shorten().to_string();
201 if let Ok(id) = reference.clone().into_fully_peeled_id() {
202 let commit = id.object().ok().and_then(|o| o.try_into_commit().ok());
203 if let Some(commit) = commit {
204 if let Ok(time) = commit.time() {
205 out.push(TagInfo {
206 name,
207 target_sha: commit.id().to_string(),
208 when: gix_time_to_chrono(time),
209 });
210 }
211 }
212 }
213 }
214 Ok(out)
215 }
216
217 pub fn branch_names(&self) -> Result<Vec<String>> {
219 let mut out = Vec::new();
220 let platform = self.repo.references()?;
221 for reference in platform.local_branches()?.flatten() {
222 out.push(reference.name().shorten().to_string());
223 }
224 for reference in self.repo.references()?.remote_branches()?.flatten() {
225 out.push(reference.name().shorten().to_string());
226 }
227 Ok(out)
228 }
229
230 pub fn commits_between(&self, from: Option<&str>, to: &str) -> Result<Vec<CommitInfo>> {
233 let to_oid = self
234 .resolve(to)
235 .with_context(|| t!("git.commit_not_found", commit = to))?;
236
237 let mut platform = self.repo.rev_walk([to_oid]);
238 if let Some(f) = from {
239 if let Some(f_oid) = self.resolve(f) {
240 platform = platform.with_hidden([f_oid]);
241 }
242 }
243
244 let mut out = Vec::new();
245 for info in platform.all()? {
246 let info = info?;
247 if let Ok(commit) = self.repo.find_commit(info.id) {
248 out.push(Self::commit_info(&commit)?);
249 }
250 }
251 Ok(out)
252 }
253
254 pub fn first_parent_between(&self, from: Option<&str>, to: &str) -> Result<Vec<CommitInfo>> {
257 let to_oid = self
258 .resolve(to)
259 .with_context(|| t!("git.commit_not_found", commit = to))?;
260 let mut platform = self.repo.rev_walk([to_oid]).first_parent_only();
261 if let Some(f) = from {
262 if let Some(f_oid) = self.resolve(f) {
263 platform = platform.with_hidden([f_oid]);
264 }
265 }
266 let mut out = Vec::new();
267 for info in platform.all()? {
268 let info = info?;
269 if let Ok(commit) = self.repo.find_commit(info.id) {
270 out.push(Self::commit_info(&commit)?);
271 }
272 }
273 Ok(out)
274 }
275
276 pub fn merge_base(&self, a: &str, b: &str) -> Result<Option<String>> {
278 let (oid_a, oid_b) = match (self.resolve(a), self.resolve(b)) {
279 (Some(x), Some(y)) => (x, y),
280 _ => return Ok(None),
281 };
282 match self.repo.merge_base(oid_a, oid_b) {
283 Ok(base) => Ok(Some(base.to_string())),
284 Err(_) => Ok(None),
285 }
286 }
287
288 pub fn is_ancestor_of_head(&self, sha: &str) -> Result<bool> {
290 let head = self.head_commit()?;
291 self.is_ancestor_of(sha, &head.sha)
292 }
293
294 pub fn is_ancestor_of(&self, ancestor: &str, descendant: &str) -> Result<bool> {
296 let (a, d) = match (self.resolve(ancestor), self.resolve(descendant)) {
297 (Some(a), Some(d)) => (a, d),
298 _ => return Ok(false),
299 };
300 if a == d {
301 return Ok(true);
302 }
303 match self.repo.merge_base(a, d) {
304 Ok(base) => Ok(base.detach() == a),
305 Err(_) => Ok(false),
306 }
307 }
308
309 pub fn changed_paths_for_commit(&self, sha: &str) -> Vec<String> {
312 (|| -> Option<Vec<String>> {
313 let oid = self.resolve(sha)?;
314 let commit = self.repo.find_commit(oid).ok()?;
315 let new_tree = commit.tree().ok()?;
316 let parent = commit
317 .parent_ids()
318 .next()
319 .and_then(|pid| self.repo.find_commit(pid).ok())?;
320 let old_tree = parent.tree().ok()?;
321
322 let mut paths: Vec<String> = Vec::new();
323 let mut platform = old_tree.changes().ok()?;
324 platform.options(|o| {
327 o.track_path();
328 o.track_rewrites(None);
329 });
330 let _ = platform.for_each_to_obtain_tree(&new_tree, |change| {
331 paths.push(change.location().to_string());
332 Ok::<_, std::convert::Infallible>(std::ops::ControlFlow::Continue(()))
333 });
334 Some(paths)
335 })()
336 .unwrap_or_default()
337 }
338
339 pub fn commit_info_of(&self, spec: &str) -> Option<CommitInfo> {
341 let id = self.resolve(spec)?;
342 let commit = self.repo.find_commit(id).ok()?;
343 Self::commit_info(&commit).ok()
344 }
345
346 pub fn local_branch_names(&self) -> Result<Vec<String>> {
348 let mut out = Vec::new();
349 let platform = self.repo.references()?;
350 for reference in platform.local_branches()?.flatten() {
351 out.push(reference.name().shorten().to_string());
352 }
353 out.sort();
354 Ok(out)
355 }
356
357 pub fn create_tag(&self, name: &str, target_spec: Option<&str>) -> Result<()> {
359 let target = match target_spec {
360 Some(s) => self
361 .resolve(s)
362 .with_context(|| t!("git.target_commit_not_found").to_string())?,
363 None => self.repo.head_commit()?.id,
364 };
365 self.repo
366 .reference(
367 format!("refs/tags/{name}"),
368 target,
369 gix::refs::transaction::PreviousValue::MustNotExist,
370 format!("gitversion: create tag {name}"),
371 )
372 .with_context(|| t!("git.tag_create_failed", name = name))?;
373 Ok(())
374 }
375
376 pub fn create_branch(&self, name: &str, target_spec: Option<&str>) -> Result<()> {
378 let target = match target_spec {
379 Some(s) => self
380 .resolve(s)
381 .with_context(|| t!("git.target_commit_not_found").to_string())?,
382 None => self.repo.head_commit()?.id,
383 };
384 self.repo
385 .reference(
386 format!("refs/heads/{name}"),
387 target,
388 gix::refs::transaction::PreviousValue::MustNotExist,
389 format!("gitversion: create branch {name}"),
390 )
391 .with_context(|| t!("git.branch_create_failed", name = name))?;
392 Ok(())
393 }
394
395 pub fn clear_cache(&self) -> Result<usize> {
397 let dir = self.git_dir().join("gitversion_cache");
398 if !dir.exists() {
399 return Ok(0);
400 }
401 let count = std::fs::read_dir(&dir).map(|d| d.count()).unwrap_or(0);
402 std::fs::remove_dir_all(&dir)
403 .with_context(|| t!("git.cache_clear_failed", path = dir.display()))?;
404 Ok(count)
405 }
406
407 pub fn uncommitted_changes(&self) -> Result<i64> {
409 let status = match self.repo.status(gix::progress::Discard) {
413 Ok(s) => s,
414 Err(_) => return Ok(0),
415 };
416 let iter = match status.into_index_worktree_iter(Vec::new()) {
417 Ok(it) => it,
418 Err(_) => return Ok(0),
419 };
420 Ok(iter.flatten().count() as i64)
421 }
422
423 pub fn tags_on_commit(&self, sha: &str) -> Result<HashSet<String>> {
425 Ok(self
426 .tags()?
427 .into_iter()
428 .filter(|t| t.target_sha == sha)
429 .map(|t| t.name)
430 .collect())
431 }
432}