kodegen_tools_git/operations/
tag.rs1use crate::{GitError, GitResult, RepoHandle};
6use chrono::{DateTime, Utc};
7use gix::bstr::ByteSlice;
8
9#[derive(Debug, Clone)]
11pub struct TagOpts {
12 pub name: String,
14 pub message: Option<String>,
16 pub target: Option<String>,
18 pub force: bool,
20}
21
22#[derive(Debug, Clone)]
24pub struct TagInfo {
25 pub name: String,
27 pub message: Option<String>,
29 pub target_commit: String,
31 pub timestamp: DateTime<Utc>,
33 pub is_annotated: bool,
35}
36
37pub async fn create_tag(repo: &RepoHandle, opts: TagOpts) -> GitResult<TagInfo> {
67 let repo_clone = repo.clone_inner();
68
69 tokio::task::spawn_blocking(move || {
70 let tag_ref_name = format!("refs/tags/{}", opts.name);
71
72 let target = if let Some(ref target_str) = opts.target {
74 repo_clone
75 .rev_parse_single(target_str.as_bytes().as_bstr())
76 .map_err(|e| GitError::Parse(format!("Invalid target '{target_str}': {e}")))?
77 .into()
78 } else {
79 let mut head = repo_clone.head().map_err(|e| GitError::Gix(Box::new(e)))?;
81 head.try_peel_to_id()
82 .map_err(|e| GitError::Gix(Box::new(e)))?
83 .ok_or_else(|| {
84 GitError::InvalidInput("HEAD does not point to a commit".to_string())
85 })?
86 .detach()
87 };
88
89 if !opts.force
91 && repo_clone
92 .refs
93 .find(tag_ref_name.as_bytes().as_bstr())
94 .is_ok()
95 {
96 return Err(GitError::InvalidInput(format!(
97 "Tag '{}' already exists",
98 opts.name
99 )));
100 }
101
102 let is_annotated = opts.message.is_some();
104
105 if is_annotated {
107 let message = opts.message.as_deref().unwrap_or("");
109 let signature = get_signature(&repo_clone)?;
110
111 use gix::bstr::ByteSlice;
112 let time_str = signature.time.to_string();
113 let sig_ref = gix::actor::SignatureRef {
114 name: signature.name.as_bstr(),
115 email: signature.email.as_bstr(),
116 time: &time_str,
117 };
118
119 repo_clone
120 .tag(
121 &opts.name,
122 target,
123 gix::objs::Kind::Commit,
124 Some(sig_ref),
125 message,
126 if opts.force {
127 gix::refs::transaction::PreviousValue::Any
128 } else {
129 gix::refs::transaction::PreviousValue::MustNotExist
130 },
131 )
132 .map_err(|e| GitError::Gix(Box::new(e)))?;
133 } else {
134 let ref_name = gix::refs::FullName::try_from(tag_ref_name.as_bytes().as_bstr())
135 .map_err(|e| GitError::Gix(Box::new(e)))?;
136
137 let edit = gix::refs::transaction::RefEdit {
138 change: gix::refs::transaction::Change::Update {
139 log: gix::refs::transaction::LogChange::default(),
140 expected: if opts.force {
141 gix::refs::transaction::PreviousValue::Any
142 } else {
143 gix::refs::transaction::PreviousValue::MustNotExist
144 },
145 new: gix::refs::Target::Object(target),
146 },
147 name: ref_name,
148 deref: false,
149 };
150
151 repo_clone
152 .refs
153 .transaction()
154 .prepare(
155 vec![edit],
156 gix::lock::acquire::Fail::Immediately,
157 gix::lock::acquire::Fail::Immediately,
158 )
159 .map_err(|e| GitError::Gix(Box::new(e)))?
160 .commit(None)
161 .map_err(|e| GitError::Gix(Box::new(e)))?;
162 }
163
164 let commit = repo_clone
166 .find_object(target)
167 .map_err(|e| GitError::Gix(Box::new(e)))?
168 .try_into_commit()
169 .map_err(|_| GitError::Parse("Target is not a commit".to_string()))?;
170
171 let commit_time = commit.time().map_err(|e| GitError::Gix(Box::new(e)))?;
172 let timestamp = DateTime::from_timestamp(commit_time.seconds, 0).unwrap_or_else(Utc::now);
173
174 Ok(TagInfo {
175 name: opts.name,
176 message: opts.message,
177 target_commit: target.to_string(),
178 timestamp,
179 is_annotated,
180 })
181 })
182 .await
183 .map_err(|e| GitError::Gix(Box::new(e)))?
184}
185
186pub async fn delete_tag(repo: &RepoHandle, tag_name: &str) -> GitResult<()> {
207 let repo_clone = repo.clone_inner();
208 let tag_name = tag_name.to_string();
209
210 tokio::task::spawn_blocking(move || {
211 let tag_ref_name = format!("refs/tags/{tag_name}");
212
213 repo_clone
215 .refs
216 .find(tag_ref_name.as_bytes().as_bstr())
217 .map_err(|_| GitError::ReferenceNotFound(tag_name.clone()))?;
218
219 let ref_name = gix::refs::FullName::try_from(tag_ref_name.as_bytes().as_bstr())
221 .map_err(|e| GitError::Gix(Box::new(e)))?;
222
223 let edit = gix::refs::transaction::RefEdit {
224 change: gix::refs::transaction::Change::Delete {
225 expected: gix::refs::transaction::PreviousValue::Any,
226 log: gix::refs::transaction::RefLog::AndReference,
227 },
228 name: ref_name,
229 deref: false,
230 };
231
232 repo_clone
233 .refs
234 .transaction()
235 .prepare(
236 vec![edit],
237 gix::lock::acquire::Fail::Immediately,
238 gix::lock::acquire::Fail::Immediately,
239 )
240 .map_err(|e| GitError::Gix(Box::new(e)))?
241 .commit(None)
242 .map_err(|e| GitError::Gix(Box::new(e)))?;
243
244 Ok(())
245 })
246 .await
247 .map_err(|e| GitError::Gix(Box::new(e)))?
248}
249
250pub async fn tag_exists(repo: &RepoHandle, tag_name: &str) -> GitResult<bool> {
275 let repo_clone = repo.clone_inner();
276 let tag_name = tag_name.to_string();
277
278 tokio::task::spawn_blocking(move || {
279 let tag_ref_name = format!("refs/tags/{tag_name}");
280 Ok(repo_clone
281 .refs
282 .find(tag_ref_name.as_bytes().as_bstr())
283 .is_ok())
284 })
285 .await
286 .map_err(|e| GitError::Gix(Box::new(e)))?
287}
288
289pub async fn list_tags(repo: &RepoHandle) -> GitResult<Vec<TagInfo>> {
314 let repo_clone = repo.clone_inner();
315
316 tokio::task::spawn_blocking(move || {
317 let mut tags = Vec::new();
318
319 let refs_platform = repo_clone
321 .references()
322 .map_err(|e| GitError::Gix(Box::new(e)))?;
323 let tag_refs = refs_platform
324 .prefixed("refs/tags/")
325 .map_err(|e| GitError::Gix(Box::new(e)))?;
326
327 for reference in tag_refs {
328 let mut reference = reference.map_err(GitError::Gix)?;
329
330 let name = reference.name().as_bstr();
331 if !name.starts_with(b"refs/tags/") {
332 continue;
333 }
334
335 let tag_name = name
336 .strip_prefix(b"refs/tags/")
337 .and_then(|n| std::str::from_utf8(n).ok())
338 .ok_or_else(|| GitError::Parse("Invalid tag name".to_string()))?
339 .to_string();
340
341 let target_id = reference
343 .peel_to_id()
344 .map_err(|e| GitError::Gix(Box::new(e)))?;
345
346 let (message, is_annotated, timestamp) = if let Ok(obj) =
348 repo_clone.find_object(target_id)
349 {
350 if let Ok(tag_obj) = obj.try_into_tag() {
351 let tag_ref = tag_obj.decode().ok();
352 let msg = tag_ref.as_ref().map(|t| t.message.to_string());
353 let ts = if let Some(ref tag) = tag_ref {
354 if let Some(tagger) = &tag.tagger {
355 if let Ok(time) = tagger.time() {
356 DateTime::from_timestamp(time.seconds, 0).unwrap_or_else(Utc::now)
357 } else {
358 Utc::now()
359 }
360 } else {
361 Utc::now()
362 }
363 } else {
364 Utc::now()
365 };
366 (msg, true, ts)
367 } else if let Ok(obj2) = repo_clone.find_object(target_id) {
368 if let Ok(commit) = obj2.try_into_commit() {
369 let ts = commit
370 .time()
371 .ok()
372 .unwrap_or_else(gix::date::Time::now_local_or_utc);
373 let ts = DateTime::from_timestamp(ts.seconds, 0).unwrap_or_else(Utc::now);
374 (None, false, ts)
375 } else {
376 (None, false, Utc::now())
377 }
378 } else {
379 (None, false, Utc::now())
380 }
381 } else {
382 (None, false, Utc::now())
383 };
384
385 tags.push(TagInfo {
386 name: tag_name,
387 message,
388 target_commit: target_id.to_string(),
389 timestamp,
390 is_annotated,
391 });
392 }
393
394 Ok(tags)
395 })
396 .await
397 .map_err(|e| GitError::Gix(Box::new(e)))?
398}
399
400fn get_signature(repo: &gix::Repository) -> GitResult<gix::actor::Signature> {
402 let config = repo.config_snapshot();
403
404 let name = config
405 .string("user.name")
406 .ok_or_else(|| GitError::InvalidInput("Git user.name not configured".to_string()))?;
407
408 let email = config
409 .string("user.email")
410 .ok_or_else(|| GitError::InvalidInput("Git user.email not configured".to_string()))?;
411
412 Ok(gix::actor::Signature {
413 name: name.into_owned(),
414 email: email.into_owned(),
415 time: gix::date::Time::now_local_or_utc(),
416 })
417}