1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
//! Tags

use time::OffsetDateTime;

use crate::{error::Error, util::convert_git2_time, GitRepository};

/// A git tag
#[derive(Debug, Clone)]
pub struct Tag {
    /// ID (hash)
    pub id: String,
    /// Date
    pub date: OffsetDateTime,
    /// Name (short)
    pub name: String,
    /// Full name
    pub name_full: String,
    /// Tag message - None if lightweight tag
    pub message: Option<String>,
    /// Commit ID (hash)
    pub commit_id: String,
}

impl PartialEq for Tag {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
    }
}

impl Tag {
    /// Checks if the tag is annotated
    pub fn is_annotated(&self) -> bool {
        self.message.is_some()
    }
}

/// Retrieves all the repo tags (lightweight and annotated)
pub fn list_tags(repo: &GitRepository) -> Result<Vec<String>, Error> {
    let tags = repo.tag_names(None)?;
    let tags = tags
        .into_iter()
        .filter_map(|t| t.map(|s| s.to_string()))
        .collect();
    Ok(tags)
}

/// Retrieves all the repo tag references
///
/// This method looks for all references and finds the tags.
pub fn get_tag_refs(repo: &GitRepository) -> Result<Vec<Tag>, Error> {
    let refs = repo.references()?;

    let mut tags = vec![];
    for res in refs {
        let rf = res?;

        // resolve symbolic tags
        let rf = rf.resolve()?;

        // extract data
        let id = rf.target().unwrap().to_string();
        let full_name = rf.name().unwrap_or("__invalid__").to_string();
        let name = rf.shorthand().unwrap_or("__invalid__").to_string();

        // a tag starts with 'refs/tags'
        // if it is an annotated tag, it is possible to peel back to a Tag
        // eprintln!("ref: {full_name}");
        if !full_name.starts_with("refs/tags/") {
            // eprintln!("not a tag => skipped");
            continue;
        }
        // peel to tag to check if the ref is a tag
        // NB: lightweight tags do not have ref of their own
        let tag = rf.peel_to_tag().ok();
        let tag_message = tag.map(|t| t.message().unwrap_or("__invalid__").trim().to_string());

        // peel to find the commit
        // NB: a tag always points to a commit (itself for a lightweight tag)
        let commit = rf.peel_to_commit()?;
        let commit_id = commit.id().to_string();
        let date = convert_git2_time(commit.time())?;

        tags.push({
            Tag {
                id,
                date,
                name,
                name_full: full_name,
                message: tag_message,
                commit_id,
            }
        })
    }

    Ok(tags)
}

/// Sets an annotated tag to the HEAD
pub fn set_annotated_tag(repo: &GitRepository, tag: &str, message: &str) -> Result<(), Error> {
    let head_commit = repo.head()?.peel_to_commit()?;
    let tagger = repo.signature()?;
    let _oid = repo.tag(tag, head_commit.as_object(), &tagger, message, false)?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use crate::repo::discover_repo;

    use super::*;

    #[test]
    fn test_tags_simple() {
        let cwd = std::env::current_dir().unwrap();
        let repo = discover_repo(&cwd).unwrap();
        let tags = list_tags(&repo).unwrap();
        for tag in tags {
            eprintln!("{tag}")
        }
    }

    #[test]
    fn test_tags_refs() {
        let cwd = std::env::current_dir().unwrap();
        let repo = discover_repo(&cwd).unwrap();
        let tags = get_tag_refs(&repo).unwrap();
        for tag in tags {
            eprintln!("{}:  {} ({})", tag.id, tag.name, tag.commit_id)
        }
    }
}