irox_git_tools/
describe.rs1use std::path::Path;
6
7use git2::{DiffOptions, ObjectType, StatusOptions, StatusShow, Tag};
8
9use crate::error::Error;
10
11pub struct DescribeResult {
12 pub found_commit_hash: String,
13 pub description: String,
14 pub is_dirty: bool,
15 pub tag_name: Option<String>,
16 }
18
19pub fn describe<T: AsRef<Path>>(directory: T, prefix: Option<&str>) -> Result<String, Error> {
20 let start_path = directory.as_ref();
21 let repo = crate::discover_repo_or_worktree_at(start_path)?;
22 let work_dir = repo.workdir().unwrap_or(repo.path());
23 let start_rel_path = start_path.strip_prefix(work_dir).ok();
24
25 let head = crate::get_head_for_repo(&repo)?;
26 let Ok(head_tree) = head.tree() else {
27 return Error::no_tree_for_commit_err(&repo, &head);
28 };
29
30 let Ok(mut walk) = repo.revwalk() else {
31 return Error::revwalk_err(&repo);
32 };
33 if let Err(_e) = walk.push(head.id()) {
34 return Error::id_not_in_repo_err(&repo, &head);
35 }
36
37 let status = repo
38 .statuses(Some(
39 StatusOptions::new()
40 .show(StatusShow::IndexAndWorkdir)
41 .update_index(true)
42 .include_untracked(true),
43 ))
44 .map_err(|e| Error::CommandError {
45 path: repo.path().display().to_string(),
46 cmd: "status".to_string(),
47 error: e.to_string(),
48 })?;
49 let is_dirty = !status.is_empty();
50 let dirty = if is_dirty { "-dirty" } else { "" };
51 let prefix = prefix.map(|p| format!("{p}-")).unwrap_or_default();
52
53 let mut tags: Vec<Tag> = Vec::new();
54 repo.tag_foreach(|oid, _name| {
55 if let Ok(tag) = repo.find_tag(oid) {
56 tags.push(tag);
57 }
58 true
59 })
60 .map_err(|e| Error::CommandError {
61 path: repo.path().display().to_string(),
62 cmd: "tag_foreach".to_string(),
63 error: e.to_string(),
64 })?;
65
66 for elem in walk {
67 let elem = elem.map_err(|_e| Error::revwalk(&repo))?;
68 let commit_obj = repo
69 .find_object(elem, Some(ObjectType::Commit))
70 .map_err(|_e| Error::id_not_in_repo(&repo, &elem))?;
71 let commit = commit_obj
72 .peel_to_commit()
73 .map_err(|_e| Error::id_not_in_repo(&repo, &commit_obj.id()))?;
74 let older_tree = commit
75 .tree()
76 .map_err(|_e| Error::no_tree_for_commit(&repo, &commit))?;
77 let short_id = commit_obj
78 .short_id()
79 .ok()
80 .map(|idbuf| String::from_utf8_lossy(idbuf.as_ref()).to_string());
81 let id = short_id.unwrap_or("?".to_string());
82 let mut opts = DiffOptions::new();
83 if let Some(pathspec) = start_rel_path {
84 opts.pathspec(pathspec);
85 }
86
87 let diff = repo
88 .diff_tree_to_tree(Some(&older_tree), Some(&head_tree), Some(&mut opts))
89 .map_err(|_e| Error::diff_tree(&repo, &older_tree, &head_tree))?;
90 if diff.get_delta(0).is_some() {
91 let res = format!("{prefix}g{id}{dirty}");
94 return Ok(res);
95 }
96 }
97 Ok("Unable to describe repo".to_string())
98}