git2/
tag.rs

1use std::ffi::CString;
2use std::marker;
3use std::mem;
4use std::ptr;
5use std::str;
6
7use crate::util::Binding;
8use crate::{call, raw, signature, Error, Object, ObjectType, Oid, Signature};
9
10/// A structure to represent a git [tag][1]
11///
12/// [1]: http://git-scm.com/book/en/Git-Basics-Tagging
13pub struct Tag<'repo> {
14    raw: *mut raw::git_tag,
15    _marker: marker::PhantomData<Object<'repo>>,
16}
17
18impl<'repo> Tag<'repo> {
19    /// Determine whether a tag name is valid, meaning that (when prefixed with refs/tags/) that
20    /// it is a valid reference name, and that any additional tag name restrictions are imposed
21    /// (eg, it cannot start with a -).
22    pub fn is_valid_name(tag_name: &str) -> bool {
23        crate::init();
24        let tag_name = CString::new(tag_name).unwrap();
25        let mut valid: libc::c_int = 0;
26        unsafe {
27            call::c_try(raw::git_tag_name_is_valid(&mut valid, tag_name.as_ptr())).unwrap();
28        }
29        valid == 1
30    }
31
32    /// Get the id (SHA1) of a repository tag
33    pub fn id(&self) -> Oid {
34        unsafe { Binding::from_raw(raw::git_tag_id(&*self.raw)) }
35    }
36
37    /// Get the message of a tag
38    ///
39    /// Returns None if there is no message or if it is not valid utf8
40    pub fn message(&self) -> Option<&str> {
41        self.message_bytes().and_then(|s| str::from_utf8(s).ok())
42    }
43
44    /// Get the message of a tag
45    ///
46    /// Returns None if there is no message
47    pub fn message_bytes(&self) -> Option<&[u8]> {
48        unsafe { crate::opt_bytes(self, raw::git_tag_message(&*self.raw)) }
49    }
50
51    /// Get the name of a tag
52    ///
53    /// Returns None if it is not valid utf8
54    pub fn name(&self) -> Option<&str> {
55        str::from_utf8(self.name_bytes()).ok()
56    }
57
58    /// Get the name of a tag
59    pub fn name_bytes(&self) -> &[u8] {
60        unsafe { crate::opt_bytes(self, raw::git_tag_name(&*self.raw)).unwrap() }
61    }
62
63    /// Recursively peel a tag until a non tag git_object is found
64    pub fn peel(&self) -> Result<Object<'repo>, Error> {
65        let mut ret = ptr::null_mut();
66        unsafe {
67            try_call!(raw::git_tag_peel(&mut ret, &*self.raw));
68            Ok(Binding::from_raw(ret))
69        }
70    }
71
72    /// Get the tagger (author) of a tag
73    ///
74    /// If the author is unspecified, then `None` is returned.
75    pub fn tagger(&self) -> Option<Signature<'_>> {
76        unsafe {
77            let ptr = raw::git_tag_tagger(&*self.raw);
78            if ptr.is_null() {
79                None
80            } else {
81                Some(signature::from_raw_const(self, ptr))
82            }
83        }
84    }
85
86    /// Get the tagged object of a tag
87    ///
88    /// This method performs a repository lookup for the given object and
89    /// returns it
90    pub fn target(&self) -> Result<Object<'repo>, Error> {
91        let mut ret = ptr::null_mut();
92        unsafe {
93            try_call!(raw::git_tag_target(&mut ret, &*self.raw));
94            Ok(Binding::from_raw(ret))
95        }
96    }
97
98    /// Get the OID of the tagged object of a tag
99    pub fn target_id(&self) -> Oid {
100        unsafe { Binding::from_raw(raw::git_tag_target_id(&*self.raw)) }
101    }
102
103    /// Get the ObjectType of the tagged object of a tag
104    pub fn target_type(&self) -> Option<ObjectType> {
105        unsafe { ObjectType::from_raw(raw::git_tag_target_type(&*self.raw)) }
106    }
107
108    /// Casts this Tag to be usable as an `Object`
109    pub fn as_object(&self) -> &Object<'repo> {
110        unsafe { &*(self as *const _ as *const Object<'repo>) }
111    }
112
113    /// Consumes Tag to be returned as an `Object`
114    pub fn into_object(self) -> Object<'repo> {
115        assert_eq!(mem::size_of_val(&self), mem::size_of::<Object<'_>>());
116        unsafe { mem::transmute(self) }
117    }
118}
119
120impl<'repo> std::fmt::Debug for Tag<'repo> {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
122        let mut ds = f.debug_struct("Tag");
123        if let Some(name) = self.name() {
124            ds.field("name", &name);
125        }
126        ds.field("id", &self.id());
127        ds.finish()
128    }
129}
130
131impl<'repo> Binding for Tag<'repo> {
132    type Raw = *mut raw::git_tag;
133    unsafe fn from_raw(raw: *mut raw::git_tag) -> Tag<'repo> {
134        Tag {
135            raw,
136            _marker: marker::PhantomData,
137        }
138    }
139    fn raw(&self) -> *mut raw::git_tag {
140        self.raw
141    }
142}
143
144impl<'repo> Clone for Tag<'repo> {
145    fn clone(&self) -> Self {
146        self.as_object().clone().into_tag().ok().unwrap()
147    }
148}
149
150impl<'repo> Drop for Tag<'repo> {
151    fn drop(&mut self) {
152        unsafe { raw::git_tag_free(self.raw) }
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use crate::Tag;
159
160    // Reference -- https://git-scm.com/docs/git-check-ref-format
161    #[test]
162    fn name_is_valid() {
163        assert_eq!(Tag::is_valid_name("blah_blah"), true);
164        assert_eq!(Tag::is_valid_name("v1.2.3"), true);
165        assert_eq!(Tag::is_valid_name("my/tag"), true);
166        assert_eq!(Tag::is_valid_name("@"), true);
167
168        assert_eq!(Tag::is_valid_name("-foo"), false);
169        assert_eq!(Tag::is_valid_name("foo:bar"), false);
170        assert_eq!(Tag::is_valid_name("foo^bar"), false);
171        assert_eq!(Tag::is_valid_name("foo."), false);
172        assert_eq!(Tag::is_valid_name("@{"), false);
173        assert_eq!(Tag::is_valid_name("as\\cd"), false);
174    }
175
176    #[test]
177    #[should_panic]
178    fn is_valid_name_for_invalid_tag() {
179        Tag::is_valid_name("ab\012");
180    }
181
182    #[test]
183    fn smoke() {
184        let (_td, repo) = crate::test::repo_init();
185        let head = repo.head().unwrap();
186        let id = head.target().unwrap();
187        assert!(repo.find_tag(id).is_err());
188
189        let obj = repo.find_object(id, None).unwrap();
190        let sig = repo.signature().unwrap();
191        let tag_id = repo.tag("foo", &obj, &sig, "msg", false).unwrap();
192        let tag = repo.find_tag(tag_id).unwrap();
193        assert_eq!(tag.id(), tag_id);
194
195        let tags = repo.tag_names(None).unwrap();
196        assert_eq!(tags.len(), 1);
197        assert_eq!(tags.get(0), Some("foo"));
198
199        assert_eq!(tag.name(), Some("foo"));
200        assert_eq!(tag.message(), Some("msg"));
201        assert_eq!(tag.peel().unwrap().id(), obj.id());
202        assert_eq!(tag.target_id(), obj.id());
203        assert_eq!(tag.target_type(), Some(crate::ObjectType::Commit));
204
205        assert_eq!(tag.tagger().unwrap().name(), sig.name());
206        tag.target().unwrap();
207        tag.into_object();
208
209        repo.find_object(tag_id, None).unwrap().as_tag().unwrap();
210        repo.find_object(tag_id, None)
211            .unwrap()
212            .into_tag()
213            .ok()
214            .unwrap();
215
216        repo.tag_delete("foo").unwrap();
217    }
218
219    #[test]
220    fn lite() {
221        let (_td, repo) = crate::test::repo_init();
222        let head = t!(repo.head());
223        let id = head.target().unwrap();
224        let obj = t!(repo.find_object(id, None));
225        let tag_id = t!(repo.tag_lightweight("foo", &obj, false));
226        assert!(repo.find_tag(tag_id).is_err());
227        assert_eq!(t!(repo.refname_to_id("refs/tags/foo")), id);
228
229        let tags = t!(repo.tag_names(Some("f*")));
230        assert_eq!(tags.len(), 1);
231        let tags = t!(repo.tag_names(Some("b*")));
232        assert_eq!(tags.len(), 0);
233    }
234}