irox_git_tools/
describe.rs

1// SPDX-License-Identifier: MIT
2// Copyright 2023 IROX Contributors
3//
4
5use 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    // pub
17}
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            // found it.
92
93            let res = format!("{prefix}g{id}{dirty}");
94            return Ok(res);
95        }
96    }
97    Ok("Unable to describe repo".to_string())
98}