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
9#[allow(clippy::too_many_arguments)]
10pub fn tag<W: Write>(
11 wok_config: &mut config::Config,
12 umbrella: &repo::Repo,
13 stdout: &mut W,
14 tag_name: Option<&str>,
15 sign: bool,
16 message: Option<&str>,
17 push: bool,
18 all: bool,
19 include_umbrella: bool,
20 target_repos: &[std::path::PathBuf],
21) -> Result<()> {
22 let repos_to_tag: Vec<config::Repo> = if all {
24 wok_config
26 .repos
27 .iter()
28 .filter(|config_repo| {
29 !config_repo.is_skipped_for("tag")
30 || target_repos.contains(&config_repo.path)
31 })
32 .cloned()
33 .collect()
34 } else if !target_repos.is_empty() {
35 wok_config
37 .repos
38 .iter()
39 .filter(|config_repo| target_repos.contains(&config_repo.path))
40 .cloned()
41 .collect()
42 } else {
43 wok_config
45 .repos
46 .iter()
47 .filter(|config_repo| {
48 config_repo.head == umbrella.head && !config_repo.is_skipped_for("tag")
49 })
50 .cloned()
51 .collect()
52 };
53
54 let total_targets = repos_to_tag.len() + usize::from(include_umbrella);
55
56 if total_targets == 0 {
57 writeln!(stdout, "No repositories to tag")?;
58 return Ok(());
59 }
60
61 match tag_name {
62 Some(name) => {
63 writeln!(
65 stdout,
66 "Creating tag '{}' in {} repositories...",
67 name, total_targets
68 )?;
69
70 if include_umbrella {
71 match create_tag(umbrella, name, sign, message) {
72 Ok(result) => match result {
73 TagResult::Created => {
74 writeln!(stdout, "- 'umbrella': created tag '{}'", name)?;
75 },
76 TagResult::AlreadyExists => {
77 writeln!(
78 stdout,
79 "- 'umbrella': tag '{}' already exists",
80 name
81 )?;
82 },
83 },
84 Err(e) => {
85 writeln!(
86 stdout,
87 "- 'umbrella': failed to create tag '{}' - {}",
88 name, e
89 )?;
90 },
91 }
92 }
93
94 for config_repo in &repos_to_tag {
95 if let Some(subrepo) = umbrella.get_subrepo_by_path(&config_repo.path) {
96 match create_tag(subrepo, name, sign, message) {
97 Ok(result) => match result {
98 TagResult::Created => {
99 writeln!(
100 stdout,
101 "- '{}': created tag '{}'",
102 config_repo.path.display(),
103 name
104 )?;
105 },
106 TagResult::AlreadyExists => {
107 writeln!(
108 stdout,
109 "- '{}': tag '{}' already exists",
110 config_repo.path.display(),
111 name
112 )?;
113 },
114 },
115 Err(e) => {
116 writeln!(
117 stdout,
118 "- '{}': failed to create tag '{}' - {}",
119 config_repo.path.display(),
120 name,
121 e
122 )?;
123 },
124 }
125 }
126 }
127 },
128 None => {
129 writeln!(stdout, "Listing tags in {} repositories...", total_targets)?;
131
132 if include_umbrella {
133 match list_tags(umbrella) {
134 Ok(tags) => {
135 if tags.is_empty() {
136 writeln!(stdout, "- 'umbrella': no tags found")?;
137 } else {
138 writeln!(stdout, "- 'umbrella': {}", tags.join(", "))?;
139 }
140 },
141 Err(e) => {
142 writeln!(stdout, "- 'umbrella': failed to list tags - {}", e)?;
143 },
144 }
145 }
146
147 for config_repo in &repos_to_tag {
148 if let Some(subrepo) = umbrella.get_subrepo_by_path(&config_repo.path) {
149 match list_tags(subrepo) {
150 Ok(tags) => {
151 if tags.is_empty() {
152 writeln!(
153 stdout,
154 "- '{}': no tags found",
155 config_repo.path.display()
156 )?;
157 } else {
158 writeln!(
159 stdout,
160 "- '{}': {}",
161 config_repo.path.display(),
162 tags.join(", ")
163 )?;
164 }
165 },
166 Err(e) => {
167 writeln!(
168 stdout,
169 "- '{}': failed to list tags - {}",
170 config_repo.path.display(),
171 e
172 )?;
173 },
174 }
175 }
176 }
177 },
178 }
179
180 if push {
182 writeln!(stdout, "Pushing tags to remotes...")?;
183
184 if include_umbrella {
185 match push_tags(umbrella) {
186 Ok(PushResult::Pushed) => {
187 writeln!(stdout, "- 'umbrella': pushed tags")?;
188 },
189 Ok(PushResult::Skipped) => {
190 writeln!(stdout, "- 'umbrella': no tags to push")?;
191 },
192 Err(e) => {
193 writeln!(stdout, "- 'umbrella': failed to push tags - {}", e)?;
194 },
195 }
196 }
197
198 for config_repo in &repos_to_tag {
199 if let Some(subrepo) = umbrella.get_subrepo_by_path(&config_repo.path) {
200 match push_tags(subrepo) {
201 Ok(PushResult::Pushed) => {
202 writeln!(
203 stdout,
204 "- '{}': pushed tags",
205 config_repo.path.display()
206 )?;
207 },
208 Ok(PushResult::Skipped) => {
209 writeln!(
210 stdout,
211 "- '{}': no tags to push",
212 config_repo.path.display()
213 )?;
214 },
215 Err(e) => {
216 writeln!(
217 stdout,
218 "- '{}': failed to push tags - {}",
219 config_repo.path.display(),
220 e
221 )?;
222 },
223 }
224 }
225 }
226 }
227
228 writeln!(
229 stdout,
230 "Successfully processed {} repositories",
231 total_targets
232 )?;
233 Ok(())
234}
235
236#[derive(Debug, Clone, PartialEq)]
237enum TagResult {
238 Created,
239 AlreadyExists,
240}
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq)]
243enum PushResult {
244 Pushed,
245 Skipped,
246}
247
248fn create_tag(
249 repo: &repo::Repo,
250 tag_name: &str,
251 sign: bool,
252 message: Option<&str>,
253) -> Result<TagResult> {
254 if repo
256 .git_repo
257 .revparse_single(&format!("refs/tags/{}", tag_name))
258 .is_ok()
259 {
260 return Ok(TagResult::AlreadyExists);
261 }
262
263 let head = repo.git_repo.head()?;
265 let commit = head.peel_to_commit()?;
266 let commit_obj = commit.as_object();
267
268 if sign || message.is_some() {
270 let signature = repo.git_repo.signature()?;
272 let default_message = format!("Tag {}", tag_name);
273 let tag_message = message.unwrap_or(&default_message);
274 let _tag_ref = repo.git_repo.tag(
275 tag_name,
276 commit_obj,
277 &signature,
278 tag_message,
279 sign, )?;
281 } else {
282 let _tag_ref = repo.git_repo.tag_lightweight(tag_name, commit_obj, false)?;
284 }
285
286 Ok(TagResult::Created)
287}
288
289fn list_tags(repo: &repo::Repo) -> Result<Vec<String>> {
290 let mut tags = Vec::new();
291
292 let tag_names = repo.git_repo.tag_names(None)?;
294
295 for tag_name in tag_names.iter().flatten() {
296 tags.push(tag_name.to_string());
297 }
298
299 tags.sort();
301
302 Ok(tags)
303}
304
305fn push_tags(repo: &repo::Repo) -> Result<PushResult> {
306 let head_ref = repo.git_repo.head()?;
308 let branch_name = head_ref.shorthand().with_context(|| {
309 format!(
310 "Cannot get branch name for repo at `{}`",
311 repo.work_dir.display()
312 )
313 })?;
314
315 let remote_name = repo.get_remote_name_for_branch(branch_name)?;
316
317 let mut remote = match repo.git_repo.find_remote(&remote_name) {
319 Ok(remote) => remote,
320 Err(_) => {
321 return Err(anyhow!("No remote '{}' configured", remote_name));
322 },
323 };
324
325 let tag_names = repo.git_repo.tag_names(None)?;
327 if tag_names.is_empty() {
328 return Ok(PushResult::Skipped);
329 }
330
331 let connection = remote.connect_auth(
333 git2::Direction::Push,
334 Some(repo.remote_callbacks()?),
335 None,
336 )?;
337
338 let remote_tags =
339 match panic::catch_unwind(AssertUnwindSafe(|| -> Result<_, git2::Error> {
340 let mut tags = HashMap::new();
341 for head in connection.list()?.iter() {
342 let name = head.name();
343 if name.starts_with("refs/tags/") {
344 tags.insert(name.to_string(), head.oid());
345 }
346 }
347 Ok(tags)
348 })) {
349 Ok(Ok(tags)) => tags,
350 Ok(Err(err)) => return Err(err.into()),
351 Err(_) => HashMap::new(),
352 };
353 drop(connection);
354
355 let mut refspecs: Vec<String> = Vec::new();
356 for tag_name in tag_names.iter().flatten() {
357 let refname = format!("refs/tags/{tag_name}");
358 let reference = repo.git_repo.find_reference(&refname)?;
359 let target_oid = reference.target().with_context(|| {
360 format!("Tag '{}' does not point to an object", tag_name)
361 })?;
362
363 match remote_tags.get(&refname) {
364 Some(remote_oid) if *remote_oid == target_oid => {
365 },
367 _ => refspecs.push(format!("{refname}:{refname}")),
368 }
369 }
370
371 if refspecs.is_empty() {
372 return Ok(PushResult::Skipped);
373 }
374
375 let refspec_refs: Vec<&str> =
376 refspecs.iter().map(|refspec| refspec.as_str()).collect();
377 let mut push_options = git2::PushOptions::new();
378 push_options.remote_callbacks(repo.remote_callbacks()?);
379
380 let push_result = remote.push(&refspec_refs, Some(&mut push_options));
381 let disconnect_result = remote.disconnect();
382 push_result?;
383 disconnect_result?;
384
385 Ok(PushResult::Pushed)
386}