1use anyhow::*;
2use std::collections::HashMap;
3use std::io::Write;
4use std::panic::{self, AssertUnwindSafe};
5use std::result::Result::Ok;
6
7use crate::{config, repo};
8
9fn determine_repos_to_operate_on(
11 wok_config: &config::Config,
12 umbrella: &repo::Repo,
13 all: bool,
14 target_repos: &[std::path::PathBuf],
15) -> Vec<config::Repo> {
16 if all {
17 wok_config
19 .repos
20 .iter()
21 .filter(|config_repo| {
22 !config_repo.is_skipped_for("tag")
23 || target_repos.contains(&config_repo.path)
24 })
25 .cloned()
26 .collect()
27 } else if !target_repos.is_empty() {
28 wok_config
30 .repos
31 .iter()
32 .filter(|config_repo| target_repos.contains(&config_repo.path))
33 .cloned()
34 .collect()
35 } else {
36 wok_config
38 .repos
39 .iter()
40 .filter(|config_repo| {
41 config_repo.head == umbrella.head && !config_repo.is_skipped_for("tag")
42 })
43 .cloned()
44 .collect()
45 }
46}
47
48pub fn tag_list<W: Write>(
50 wok_config: &config::Config,
51 umbrella: &repo::Repo,
52 stdout: &mut W,
53 all: bool,
54 include_umbrella: bool,
55 target_repos: &[std::path::PathBuf],
56) -> Result<()> {
57 let repos_to_tag =
58 determine_repos_to_operate_on(wok_config, umbrella, all, target_repos);
59 let total_targets = repos_to_tag.len() + usize::from(include_umbrella);
60
61 if total_targets == 0 {
62 writeln!(stdout, "No repositories to tag")?;
63 return Ok(());
64 }
65
66 writeln!(stdout, "Listing tags in {} repositories...", total_targets)?;
67
68 if include_umbrella {
69 match list_tags(umbrella) {
70 Ok(tags) => {
71 if tags.is_empty() {
72 writeln!(stdout, "- 'umbrella': no tags found")?;
73 } else {
74 writeln!(stdout, "- 'umbrella': {}", tags.join(", "))?;
75 }
76 },
77 Err(e) => {
78 writeln!(stdout, "- 'umbrella': failed to list tags - {}", e)?;
79 },
80 }
81 }
82
83 for config_repo in &repos_to_tag {
84 if let Some(subrepo) = umbrella.get_subrepo_by_path(&config_repo.path) {
85 match list_tags(subrepo) {
86 Ok(tags) => {
87 if tags.is_empty() {
88 writeln!(
89 stdout,
90 "- '{}': no tags found",
91 config_repo.path.display()
92 )?;
93 } else {
94 writeln!(
95 stdout,
96 "- '{}': {}",
97 config_repo.path.display(),
98 tags.join(", ")
99 )?;
100 }
101 },
102 Err(e) => {
103 writeln!(
104 stdout,
105 "- '{}': failed to list tags - {}",
106 config_repo.path.display(),
107 e
108 )?;
109 },
110 }
111 }
112 }
113
114 writeln!(
115 stdout,
116 "Successfully processed {} repositories",
117 total_targets
118 )?;
119 Ok(())
120}
121
122#[allow(clippy::too_many_arguments)]
124pub fn tag_create<W: Write>(
125 wok_config: &config::Config,
126 umbrella: &repo::Repo,
127 stdout: &mut W,
128 tag_name: &str,
129 sign: bool,
130 message: Option<&str>,
131 all: bool,
132 include_umbrella: bool,
133 updated: bool,
134 target_repos: &[std::path::PathBuf],
135) -> Result<()> {
136 let repos_to_tag =
137 determine_repos_to_operate_on(wok_config, umbrella, all, target_repos);
138
139 let repos_to_tag: Vec<config::Repo> = if updated {
141 repos_to_tag
142 .into_iter()
143 .filter(|config_repo| {
144 if let Some(subrepo) = umbrella.get_subrepo_by_path(&config_repo.path) {
145 !commit_has_tags(subrepo).unwrap_or(false)
147 } else {
148 false
150 }
151 })
152 .collect()
153 } else {
154 repos_to_tag
155 };
156
157 let total_targets = repos_to_tag.len() + usize::from(include_umbrella);
158
159 if total_targets == 0 {
160 writeln!(stdout, "No repositories to tag")?;
161 return Ok(());
162 }
163
164 writeln!(
165 stdout,
166 "Creating tag '{}' in {} repositories...",
167 tag_name, total_targets
168 )?;
169
170 if include_umbrella {
171 match create_tag(umbrella, tag_name, sign, message) {
172 Ok(result) => match result {
173 TagResult::Created => {
174 writeln!(stdout, "- 'umbrella': created tag '{}'", tag_name)?;
175 },
176 TagResult::AlreadyExists => {
177 writeln!(
178 stdout,
179 "- 'umbrella': tag '{}' already exists",
180 tag_name
181 )?;
182 },
183 },
184 Err(e) => {
185 writeln!(
186 stdout,
187 "- 'umbrella': failed to create tag '{}' - {}",
188 tag_name, e
189 )?;
190 },
191 }
192 }
193
194 for config_repo in &repos_to_tag {
195 if let Some(subrepo) = umbrella.get_subrepo_by_path(&config_repo.path) {
196 match create_tag(subrepo, tag_name, sign, message) {
197 Ok(result) => match result {
198 TagResult::Created => {
199 writeln!(
200 stdout,
201 "- '{}': created tag '{}'",
202 config_repo.path.display(),
203 tag_name
204 )?;
205 },
206 TagResult::AlreadyExists => {
207 writeln!(
208 stdout,
209 "- '{}': tag '{}' already exists",
210 config_repo.path.display(),
211 tag_name
212 )?;
213 },
214 },
215 Err(e) => {
216 writeln!(
217 stdout,
218 "- '{}': failed to create tag '{}' - {}",
219 config_repo.path.display(),
220 tag_name,
221 e
222 )?;
223 },
224 }
225 }
226 }
227
228 writeln!(
229 stdout,
230 "Successfully processed {} repositories",
231 total_targets
232 )?;
233 Ok(())
234}
235
236pub fn tag_push<W: Write>(
238 wok_config: &config::Config,
239 umbrella: &repo::Repo,
240 stdout: &mut W,
241 all: bool,
242 include_umbrella: bool,
243 target_repos: &[std::path::PathBuf],
244) -> Result<()> {
245 let repos_to_tag =
246 determine_repos_to_operate_on(wok_config, umbrella, all, target_repos);
247 let total_targets = repos_to_tag.len() + usize::from(include_umbrella);
248
249 if total_targets == 0 {
250 writeln!(stdout, "No repositories to tag")?;
251 return Ok(());
252 }
253
254 writeln!(stdout, "Pushing tags to remotes...")?;
255
256 if include_umbrella {
257 match push_tags(umbrella) {
258 Ok(PushResult::Pushed) => {
259 writeln!(stdout, "- 'umbrella': pushed tags")?;
260 },
261 Ok(PushResult::Skipped) => {
262 writeln!(stdout, "- 'umbrella': no tags to push")?;
263 },
264 Err(e) => {
265 writeln!(stdout, "- 'umbrella': failed to push tags - {}", e)?;
266 },
267 }
268 }
269
270 for config_repo in &repos_to_tag {
271 if let Some(subrepo) = umbrella.get_subrepo_by_path(&config_repo.path) {
272 match push_tags(subrepo) {
273 Ok(PushResult::Pushed) => {
274 writeln!(
275 stdout,
276 "- '{}': pushed tags",
277 config_repo.path.display()
278 )?;
279 },
280 Ok(PushResult::Skipped) => {
281 writeln!(
282 stdout,
283 "- '{}': no tags to push",
284 config_repo.path.display()
285 )?;
286 },
287 Err(e) => {
288 writeln!(
289 stdout,
290 "- '{}': failed to push tags - {}",
291 config_repo.path.display(),
292 e
293 )?;
294 },
295 }
296 }
297 }
298
299 writeln!(
300 stdout,
301 "Successfully processed {} repositories",
302 total_targets
303 )?;
304 Ok(())
305}
306
307#[allow(clippy::too_many_arguments)]
309pub fn tag<W: Write>(
310 wok_config: &mut config::Config,
311 umbrella: &repo::Repo,
312 stdout: &mut W,
313 tag_name: Option<&str>,
314 sign: bool,
315 message: Option<&str>,
316 push: bool,
317 all: bool,
318 include_umbrella: bool,
319 target_repos: &[std::path::PathBuf],
320) -> Result<()> {
321 match tag_name {
322 Some(name) => {
323 tag_create(
324 wok_config,
325 umbrella,
326 stdout,
327 name,
328 sign,
329 message,
330 all,
331 include_umbrella,
332 false, target_repos,
334 )?;
335 if push {
336 tag_push(
337 wok_config,
338 umbrella,
339 stdout,
340 all,
341 include_umbrella,
342 target_repos,
343 )?;
344 }
345 },
346 None => {
347 tag_list(
348 wok_config,
349 umbrella,
350 stdout,
351 all,
352 include_umbrella,
353 target_repos,
354 )?;
355 if push {
356 tag_push(
357 wok_config,
358 umbrella,
359 stdout,
360 all,
361 include_umbrella,
362 target_repos,
363 )?;
364 }
365 },
366 }
367 Ok(())
368}
369
370#[derive(Debug, Clone, PartialEq)]
371enum TagResult {
372 Created,
373 AlreadyExists,
374}
375
376#[derive(Debug, Clone, Copy, PartialEq, Eq)]
377enum PushResult {
378 Pushed,
379 Skipped,
380}
381
382fn create_tag(
383 repo: &repo::Repo,
384 tag_name: &str,
385 sign: bool,
386 message: Option<&str>,
387) -> Result<TagResult> {
388 if repo
390 .git_repo
391 .revparse_single(&format!("refs/tags/{}", tag_name))
392 .is_ok()
393 {
394 return Ok(TagResult::AlreadyExists);
395 }
396
397 let head = repo.git_repo.head()?;
399 let commit = head.peel_to_commit()?;
400 let commit_obj = commit.as_object();
401
402 if sign || message.is_some() {
404 let signature = repo.git_repo.signature()?;
406 let default_message = format!("Tag {}", tag_name);
407 let tag_message = message.unwrap_or(&default_message);
408 let _tag_ref = repo.git_repo.tag(
409 tag_name,
410 commit_obj,
411 &signature,
412 tag_message,
413 sign, )?;
415 } else {
416 let _tag_ref = repo.git_repo.tag_lightweight(tag_name, commit_obj, false)?;
418 }
419
420 Ok(TagResult::Created)
421}
422
423fn list_tags(repo: &repo::Repo) -> Result<Vec<String>> {
424 let mut tags = Vec::new();
425
426 let tag_names = repo.git_repo.tag_names(None)?;
428
429 for tag_name in tag_names.iter().flatten() {
430 tags.push(tag_name.to_string());
431 }
432
433 tags.sort();
435
436 Ok(tags)
437}
438
439fn commit_has_tags(repo: &repo::Repo) -> Result<bool> {
441 let head = repo.git_repo.head()?;
443 let head_oid = head.peel_to_commit()?.id();
444
445 let tag_names = repo.git_repo.tag_names(None)?;
447
448 for tag_name in tag_names.iter().flatten() {
450 let tag_ref = repo
451 .git_repo
452 .find_reference(&format!("refs/tags/{}", tag_name))?;
453
454 if let Ok(tag_commit) = tag_ref.peel_to_commit()
456 && tag_commit.id() == head_oid
457 {
458 return Ok(true);
459 }
460 }
461
462 Ok(false)
463}
464
465fn push_tags(repo: &repo::Repo) -> Result<PushResult> {
466 let head_ref = repo.git_repo.head()?;
468 let branch_name = head_ref.shorthand().with_context(|| {
469 format!(
470 "Cannot get branch name for repo at `{}`",
471 repo.work_dir.display()
472 )
473 })?;
474
475 let remote_name = repo.get_remote_name_for_branch(branch_name)?;
476
477 let mut remote = match repo.git_repo.find_remote(&remote_name) {
479 Ok(remote) => remote,
480 Err(_) => {
481 return Err(anyhow!("No remote '{}' configured", remote_name));
482 },
483 };
484
485 let tag_names = repo.git_repo.tag_names(None)?;
487 if tag_names.is_empty() {
488 return Ok(PushResult::Skipped);
489 }
490
491 let connection = remote.connect_auth(
493 git2::Direction::Push,
494 Some(repo.remote_callbacks()?),
495 None,
496 )?;
497
498 let remote_tags =
499 match panic::catch_unwind(AssertUnwindSafe(|| -> Result<_, git2::Error> {
500 let mut tags = HashMap::new();
501 for head in connection.list()?.iter() {
502 let name = head.name();
503 if name.starts_with("refs/tags/") {
504 tags.insert(name.to_string(), head.oid());
505 }
506 }
507 Ok(tags)
508 })) {
509 Ok(Ok(tags)) => tags,
510 Ok(Err(err)) => return Err(err.into()),
511 Err(_) => HashMap::new(),
512 };
513 drop(connection);
514
515 let mut refspecs: Vec<String> = Vec::new();
516 for tag_name in tag_names.iter().flatten() {
517 let refname = format!("refs/tags/{tag_name}");
518 let reference = repo.git_repo.find_reference(&refname)?;
519 let target_oid = reference.target().with_context(|| {
520 format!("Tag '{}' does not point to an object", tag_name)
521 })?;
522
523 match remote_tags.get(&refname) {
524 Some(remote_oid) if *remote_oid == target_oid => {
525 },
527 _ => refspecs.push(format!("{refname}:{refname}")),
528 }
529 }
530
531 if refspecs.is_empty() {
532 return Ok(PushResult::Skipped);
533 }
534
535 let refspec_refs: Vec<&str> =
536 refspecs.iter().map(|refspec| refspec.as_str()).collect();
537 let mut push_options = git2::PushOptions::new();
538 push_options.remote_callbacks(repo.remote_callbacks()?);
539
540 let push_result = remote.push(&refspec_refs, Some(&mut push_options));
541 let disconnect_result = remote.disconnect();
542 push_result?;
543 disconnect_result?;
544
545 Ok(PushResult::Pushed)
546}