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
122pub fn tag_create<W: Write>(
124 wok_config: &config::Config,
125 umbrella: &repo::Repo,
126 stdout: &mut W,
127 tag_name: &str,
128 sign: bool,
129 message: Option<&str>,
130 all: bool,
131 include_umbrella: bool,
132 target_repos: &[std::path::PathBuf],
133) -> Result<()> {
134 let repos_to_tag =
135 determine_repos_to_operate_on(wok_config, umbrella, all, target_repos);
136 let total_targets = repos_to_tag.len() + usize::from(include_umbrella);
137
138 if total_targets == 0 {
139 writeln!(stdout, "No repositories to tag")?;
140 return Ok(());
141 }
142
143 writeln!(
144 stdout,
145 "Creating tag '{}' in {} repositories...",
146 tag_name, total_targets
147 )?;
148
149 if include_umbrella {
150 match create_tag(umbrella, tag_name, sign, message) {
151 Ok(result) => match result {
152 TagResult::Created => {
153 writeln!(stdout, "- 'umbrella': created tag '{}'", tag_name)?;
154 },
155 TagResult::AlreadyExists => {
156 writeln!(
157 stdout,
158 "- 'umbrella': tag '{}' already exists",
159 tag_name
160 )?;
161 },
162 },
163 Err(e) => {
164 writeln!(
165 stdout,
166 "- 'umbrella': failed to create tag '{}' - {}",
167 tag_name, e
168 )?;
169 },
170 }
171 }
172
173 for config_repo in &repos_to_tag {
174 if let Some(subrepo) = umbrella.get_subrepo_by_path(&config_repo.path) {
175 match create_tag(subrepo, tag_name, sign, message) {
176 Ok(result) => match result {
177 TagResult::Created => {
178 writeln!(
179 stdout,
180 "- '{}': created tag '{}'",
181 config_repo.path.display(),
182 tag_name
183 )?;
184 },
185 TagResult::AlreadyExists => {
186 writeln!(
187 stdout,
188 "- '{}': tag '{}' already exists",
189 config_repo.path.display(),
190 tag_name
191 )?;
192 },
193 },
194 Err(e) => {
195 writeln!(
196 stdout,
197 "- '{}': failed to create tag '{}' - {}",
198 config_repo.path.display(),
199 tag_name,
200 e
201 )?;
202 },
203 }
204 }
205 }
206
207 writeln!(
208 stdout,
209 "Successfully processed {} repositories",
210 total_targets
211 )?;
212 Ok(())
213}
214
215pub fn tag_push<W: Write>(
217 wok_config: &config::Config,
218 umbrella: &repo::Repo,
219 stdout: &mut W,
220 all: bool,
221 include_umbrella: bool,
222 target_repos: &[std::path::PathBuf],
223) -> Result<()> {
224 let repos_to_tag =
225 determine_repos_to_operate_on(wok_config, umbrella, all, target_repos);
226 let total_targets = repos_to_tag.len() + usize::from(include_umbrella);
227
228 if total_targets == 0 {
229 writeln!(stdout, "No repositories to tag")?;
230 return Ok(());
231 }
232
233 writeln!(stdout, "Pushing tags to remotes...")?;
234
235 if include_umbrella {
236 match push_tags(umbrella) {
237 Ok(PushResult::Pushed) => {
238 writeln!(stdout, "- 'umbrella': pushed tags")?;
239 },
240 Ok(PushResult::Skipped) => {
241 writeln!(stdout, "- 'umbrella': no tags to push")?;
242 },
243 Err(e) => {
244 writeln!(stdout, "- 'umbrella': failed to push tags - {}", e)?;
245 },
246 }
247 }
248
249 for config_repo in &repos_to_tag {
250 if let Some(subrepo) = umbrella.get_subrepo_by_path(&config_repo.path) {
251 match push_tags(subrepo) {
252 Ok(PushResult::Pushed) => {
253 writeln!(
254 stdout,
255 "- '{}': pushed tags",
256 config_repo.path.display()
257 )?;
258 },
259 Ok(PushResult::Skipped) => {
260 writeln!(
261 stdout,
262 "- '{}': no tags to push",
263 config_repo.path.display()
264 )?;
265 },
266 Err(e) => {
267 writeln!(
268 stdout,
269 "- '{}': failed to push tags - {}",
270 config_repo.path.display(),
271 e
272 )?;
273 },
274 }
275 }
276 }
277
278 writeln!(
279 stdout,
280 "Successfully processed {} repositories",
281 total_targets
282 )?;
283 Ok(())
284}
285
286#[allow(clippy::too_many_arguments)]
288pub fn tag<W: Write>(
289 wok_config: &mut config::Config,
290 umbrella: &repo::Repo,
291 stdout: &mut W,
292 tag_name: Option<&str>,
293 sign: bool,
294 message: Option<&str>,
295 push: bool,
296 all: bool,
297 include_umbrella: bool,
298 target_repos: &[std::path::PathBuf],
299) -> Result<()> {
300 match tag_name {
301 Some(name) => {
302 tag_create(
303 wok_config,
304 umbrella,
305 stdout,
306 name,
307 sign,
308 message,
309 all,
310 include_umbrella,
311 target_repos,
312 )?;
313 if push {
314 tag_push(
315 wok_config,
316 umbrella,
317 stdout,
318 all,
319 include_umbrella,
320 target_repos,
321 )?;
322 }
323 },
324 None => {
325 tag_list(
326 wok_config,
327 umbrella,
328 stdout,
329 all,
330 include_umbrella,
331 target_repos,
332 )?;
333 if push {
334 tag_push(
335 wok_config,
336 umbrella,
337 stdout,
338 all,
339 include_umbrella,
340 target_repos,
341 )?;
342 }
343 },
344 }
345 Ok(())
346}
347
348#[derive(Debug, Clone, PartialEq)]
349enum TagResult {
350 Created,
351 AlreadyExists,
352}
353
354#[derive(Debug, Clone, Copy, PartialEq, Eq)]
355enum PushResult {
356 Pushed,
357 Skipped,
358}
359
360fn create_tag(
361 repo: &repo::Repo,
362 tag_name: &str,
363 sign: bool,
364 message: Option<&str>,
365) -> Result<TagResult> {
366 if repo
368 .git_repo
369 .revparse_single(&format!("refs/tags/{}", tag_name))
370 .is_ok()
371 {
372 return Ok(TagResult::AlreadyExists);
373 }
374
375 let head = repo.git_repo.head()?;
377 let commit = head.peel_to_commit()?;
378 let commit_obj = commit.as_object();
379
380 if sign || message.is_some() {
382 let signature = repo.git_repo.signature()?;
384 let default_message = format!("Tag {}", tag_name);
385 let tag_message = message.unwrap_or(&default_message);
386 let _tag_ref = repo.git_repo.tag(
387 tag_name,
388 commit_obj,
389 &signature,
390 tag_message,
391 sign, )?;
393 } else {
394 let _tag_ref = repo.git_repo.tag_lightweight(tag_name, commit_obj, false)?;
396 }
397
398 Ok(TagResult::Created)
399}
400
401fn list_tags(repo: &repo::Repo) -> Result<Vec<String>> {
402 let mut tags = Vec::new();
403
404 let tag_names = repo.git_repo.tag_names(None)?;
406
407 for tag_name in tag_names.iter().flatten() {
408 tags.push(tag_name.to_string());
409 }
410
411 tags.sort();
413
414 Ok(tags)
415}
416
417fn push_tags(repo: &repo::Repo) -> Result<PushResult> {
418 let head_ref = repo.git_repo.head()?;
420 let branch_name = head_ref.shorthand().with_context(|| {
421 format!(
422 "Cannot get branch name for repo at `{}`",
423 repo.work_dir.display()
424 )
425 })?;
426
427 let remote_name = repo.get_remote_name_for_branch(branch_name)?;
428
429 let mut remote = match repo.git_repo.find_remote(&remote_name) {
431 Ok(remote) => remote,
432 Err(_) => {
433 return Err(anyhow!("No remote '{}' configured", remote_name));
434 },
435 };
436
437 let tag_names = repo.git_repo.tag_names(None)?;
439 if tag_names.is_empty() {
440 return Ok(PushResult::Skipped);
441 }
442
443 let connection = remote.connect_auth(
445 git2::Direction::Push,
446 Some(repo.remote_callbacks()?),
447 None,
448 )?;
449
450 let remote_tags =
451 match panic::catch_unwind(AssertUnwindSafe(|| -> Result<_, git2::Error> {
452 let mut tags = HashMap::new();
453 for head in connection.list()?.iter() {
454 let name = head.name();
455 if name.starts_with("refs/tags/") {
456 tags.insert(name.to_string(), head.oid());
457 }
458 }
459 Ok(tags)
460 })) {
461 Ok(Ok(tags)) => tags,
462 Ok(Err(err)) => return Err(err.into()),
463 Err(_) => HashMap::new(),
464 };
465 drop(connection);
466
467 let mut refspecs: Vec<String> = Vec::new();
468 for tag_name in tag_names.iter().flatten() {
469 let refname = format!("refs/tags/{tag_name}");
470 let reference = repo.git_repo.find_reference(&refname)?;
471 let target_oid = reference.target().with_context(|| {
472 format!("Tag '{}' does not point to an object", tag_name)
473 })?;
474
475 match remote_tags.get(&refname) {
476 Some(remote_oid) if *remote_oid == target_oid => {
477 },
479 _ => refspecs.push(format!("{refname}:{refname}")),
480 }
481 }
482
483 if refspecs.is_empty() {
484 return Ok(PushResult::Skipped);
485 }
486
487 let refspec_refs: Vec<&str> =
488 refspecs.iter().map(|refspec| refspec.as_str()).collect();
489 let mut push_options = git2::PushOptions::new();
490 push_options.remote_callbacks(repo.remote_callbacks()?);
491
492 let push_result = remote.push(&refspec_refs, Some(&mut push_options));
493 let disconnect_result = remote.disconnect();
494 push_result?;
495 disconnect_result?;
496
497 Ok(PushResult::Pushed)
498}