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 push: bool,
17 all: bool,
18 target_repos: &[std::path::PathBuf],
19) -> Result<()> {
20 let repos_to_tag: Vec<config::Repo> = if all {
22 wok_config
24 .repos
25 .iter()
26 .filter(|config_repo| {
27 !config_repo.is_skipped_for("tag")
28 || target_repos.contains(&config_repo.path)
29 })
30 .cloned()
31 .collect()
32 } else if !target_repos.is_empty() {
33 wok_config
35 .repos
36 .iter()
37 .filter(|config_repo| target_repos.contains(&config_repo.path))
38 .cloned()
39 .collect()
40 } else {
41 wok_config
43 .repos
44 .iter()
45 .filter(|config_repo| {
46 config_repo.head == umbrella.head && !config_repo.is_skipped_for("tag")
47 })
48 .cloned()
49 .collect()
50 };
51
52 if repos_to_tag.is_empty() {
53 writeln!(stdout, "No repositories to tag")?;
54 return Ok(());
55 }
56
57 match tag_name {
58 Some(name) => {
59 writeln!(
61 stdout,
62 "Creating tag '{}' in {} repositories...",
63 name,
64 repos_to_tag.len()
65 )?;
66
67 for config_repo in &repos_to_tag {
68 if let Some(subrepo) = umbrella.get_subrepo_by_path(&config_repo.path) {
69 match create_tag(subrepo, name, sign) {
70 Ok(result) => match result {
71 TagResult::Created => {
72 writeln!(
73 stdout,
74 "- '{}': created tag '{}'",
75 config_repo.path.display(),
76 name
77 )?;
78 },
79 TagResult::AlreadyExists => {
80 writeln!(
81 stdout,
82 "- '{}': tag '{}' already exists",
83 config_repo.path.display(),
84 name
85 )?;
86 },
87 },
88 Err(e) => {
89 writeln!(
90 stdout,
91 "- '{}': failed to create tag '{}' - {}",
92 config_repo.path.display(),
93 name,
94 e
95 )?;
96 },
97 }
98 }
99 }
100 },
101 None => {
102 writeln!(
104 stdout,
105 "Listing tags in {} repositories...",
106 repos_to_tag.len()
107 )?;
108
109 for config_repo in &repos_to_tag {
110 if let Some(subrepo) = umbrella.get_subrepo_by_path(&config_repo.path) {
111 match list_tags(subrepo) {
112 Ok(tags) => {
113 if tags.is_empty() {
114 writeln!(
115 stdout,
116 "- '{}': no tags found",
117 config_repo.path.display()
118 )?;
119 } else {
120 writeln!(
121 stdout,
122 "- '{}': {}",
123 config_repo.path.display(),
124 tags.join(", ")
125 )?;
126 }
127 },
128 Err(e) => {
129 writeln!(
130 stdout,
131 "- '{}': failed to list tags - {}",
132 config_repo.path.display(),
133 e
134 )?;
135 },
136 }
137 }
138 }
139 },
140 }
141
142 if push {
144 writeln!(stdout, "Pushing tags to remotes...")?;
145 for config_repo in &repos_to_tag {
146 if let Some(subrepo) = umbrella.get_subrepo_by_path(&config_repo.path) {
147 match push_tags(subrepo) {
148 Ok(PushResult::Pushed) => {
149 writeln!(
150 stdout,
151 "- '{}': pushed tags",
152 config_repo.path.display()
153 )?;
154 },
155 Ok(PushResult::Skipped) => {
156 writeln!(
157 stdout,
158 "- '{}': no tags to push",
159 config_repo.path.display()
160 )?;
161 },
162 Err(e) => {
163 writeln!(
164 stdout,
165 "- '{}': failed to push tags - {}",
166 config_repo.path.display(),
167 e
168 )?;
169 },
170 }
171 }
172 }
173 }
174
175 writeln!(
176 stdout,
177 "Successfully processed {} repositories",
178 repos_to_tag.len()
179 )?;
180 Ok(())
181}
182
183#[derive(Debug, Clone, PartialEq)]
184enum TagResult {
185 Created,
186 AlreadyExists,
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190enum PushResult {
191 Pushed,
192 Skipped,
193}
194
195fn create_tag(repo: &repo::Repo, tag_name: &str, sign: bool) -> Result<TagResult> {
196 if repo
198 .git_repo
199 .revparse_single(&format!("refs/tags/{}", tag_name))
200 .is_ok()
201 {
202 return Ok(TagResult::AlreadyExists);
203 }
204
205 let head = repo.git_repo.head()?;
207 let commit = head.peel_to_commit()?;
208 let commit_obj = commit.as_object();
209
210 if sign {
212 let signature = repo.git_repo.signature()?;
214 let _tag_ref = repo.git_repo.tag(
215 tag_name,
216 commit_obj,
217 &signature,
218 &format!("Tag {}", tag_name),
219 false,
220 )?;
221 } else {
222 let _tag_ref = repo.git_repo.tag_lightweight(tag_name, commit_obj, false)?;
224 }
225
226 Ok(TagResult::Created)
227}
228
229fn list_tags(repo: &repo::Repo) -> Result<Vec<String>> {
230 let mut tags = Vec::new();
231
232 let tag_names = repo.git_repo.tag_names(None)?;
234
235 for tag_name in tag_names.iter().flatten() {
236 tags.push(tag_name.to_string());
237 }
238
239 tags.sort();
241
242 Ok(tags)
243}
244
245fn push_tags(repo: &repo::Repo) -> Result<PushResult> {
246 let head_ref = repo.git_repo.head()?;
248 let branch_name = head_ref.shorthand().with_context(|| {
249 format!(
250 "Cannot get branch name for repo at `{}`",
251 repo.work_dir.display()
252 )
253 })?;
254
255 let remote_name = repo.get_remote_name_for_branch(branch_name)?;
256
257 let mut remote = match repo.git_repo.find_remote(&remote_name) {
259 Ok(remote) => remote,
260 Err(_) => {
261 return Err(anyhow!("No remote '{}' configured", remote_name));
262 },
263 };
264
265 let tag_names = repo.git_repo.tag_names(None)?;
267 if tag_names.is_empty() {
268 return Ok(PushResult::Skipped);
269 }
270
271 let connection = remote.connect_auth(
273 git2::Direction::Push,
274 Some(repo.remote_callbacks()?),
275 None,
276 )?;
277
278 let remote_tags =
279 match panic::catch_unwind(AssertUnwindSafe(|| -> Result<_, git2::Error> {
280 let mut tags = HashMap::new();
281 for head in connection.list()?.iter() {
282 let name = head.name();
283 if name.starts_with("refs/tags/") {
284 tags.insert(name.to_string(), head.oid());
285 }
286 }
287 Ok(tags)
288 })) {
289 Ok(Ok(tags)) => tags,
290 Ok(Err(err)) => return Err(err.into()),
291 Err(_) => HashMap::new(),
292 };
293 drop(connection);
294
295 let mut refspecs: Vec<String> = Vec::new();
296 for tag_name in tag_names.iter().flatten() {
297 let refname = format!("refs/tags/{tag_name}");
298 let reference = repo.git_repo.find_reference(&refname)?;
299 let target_oid = reference.target().with_context(|| {
300 format!("Tag '{}' does not point to an object", tag_name)
301 })?;
302
303 match remote_tags.get(&refname) {
304 Some(remote_oid) if *remote_oid == target_oid => {
305 },
307 _ => refspecs.push(format!("{refname}:{refname}")),
308 }
309 }
310
311 if refspecs.is_empty() {
312 return Ok(PushResult::Skipped);
313 }
314
315 let refspec_refs: Vec<&str> =
316 refspecs.iter().map(|refspec| refspec.as_str()).collect();
317 let mut push_options = git2::PushOptions::new();
318 push_options.remote_callbacks(repo.remote_callbacks()?);
319
320 let push_result = remote.push(&refspec_refs, Some(&mut push_options));
321 let disconnect_result = remote.disconnect();
322 push_result?;
323 disconnect_result?;
324
325 Ok(PushResult::Pushed)
326}