1use anyhow::*;
2use std::io::Write;
3use std::result::Result::Ok;
4
5use crate::{config, repo};
6
7#[allow(clippy::too_many_arguments)]
8pub fn tag<W: Write>(
9 wok_config: &mut config::Config,
10 umbrella: &repo::Repo,
11 stdout: &mut W,
12 tag_name: Option<&str>,
13 sign: bool,
14 push: bool,
15 all: bool,
16 target_repos: &[std::path::PathBuf],
17) -> Result<()> {
18 let repos_to_tag = if all {
20 wok_config.repos.clone()
22 } else if !target_repos.is_empty() {
23 wok_config
25 .repos
26 .iter()
27 .filter(|config_repo| target_repos.contains(&config_repo.path))
28 .cloned()
29 .collect()
30 } else {
31 wok_config
33 .repos
34 .iter()
35 .filter(|config_repo| config_repo.head == umbrella.head)
36 .cloned()
37 .collect()
38 };
39
40 if repos_to_tag.is_empty() {
41 writeln!(stdout, "No repositories to tag")?;
42 return Ok(());
43 }
44
45 match tag_name {
46 Some(name) => {
47 writeln!(
49 stdout,
50 "Creating tag '{}' in {} repositories...",
51 name,
52 repos_to_tag.len()
53 )?;
54
55 for config_repo in &repos_to_tag {
56 if let Some(subrepo) = umbrella.get_subrepo_by_path(&config_repo.path) {
57 match create_tag(subrepo, name, sign) {
58 Ok(result) => match result {
59 TagResult::Created => {
60 writeln!(
61 stdout,
62 "- '{}': created tag '{}'",
63 config_repo.path.display(),
64 name
65 )?;
66 },
67 TagResult::AlreadyExists => {
68 writeln!(
69 stdout,
70 "- '{}': tag '{}' already exists",
71 config_repo.path.display(),
72 name
73 )?;
74 },
75 },
76 Err(e) => {
77 writeln!(
78 stdout,
79 "- '{}': failed to create tag '{}' - {}",
80 config_repo.path.display(),
81 name,
82 e
83 )?;
84 },
85 }
86 }
87 }
88 },
89 None => {
90 writeln!(
92 stdout,
93 "Listing tags in {} repositories...",
94 repos_to_tag.len()
95 )?;
96
97 for config_repo in &repos_to_tag {
98 if let Some(subrepo) = umbrella.get_subrepo_by_path(&config_repo.path) {
99 match list_tags(subrepo) {
100 Ok(tags) => {
101 if tags.is_empty() {
102 writeln!(
103 stdout,
104 "- '{}': no tags found",
105 config_repo.path.display()
106 )?;
107 } else {
108 writeln!(
109 stdout,
110 "- '{}': {}",
111 config_repo.path.display(),
112 tags.join(", ")
113 )?;
114 }
115 },
116 Err(e) => {
117 writeln!(
118 stdout,
119 "- '{}': failed to list tags - {}",
120 config_repo.path.display(),
121 e
122 )?;
123 },
124 }
125 }
126 }
127 },
128 }
129
130 if push {
132 writeln!(stdout, "Pushing tags to remotes...")?;
133 for config_repo in &repos_to_tag {
134 if let Some(subrepo) = umbrella.get_subrepo_by_path(&config_repo.path) {
135 match push_tags(subrepo) {
136 Ok(_) => {
137 writeln!(
138 stdout,
139 "- '{}': pushed tags",
140 config_repo.path.display()
141 )?;
142 },
143 Err(e) => {
144 writeln!(
145 stdout,
146 "- '{}': failed to push tags - {}",
147 config_repo.path.display(),
148 e
149 )?;
150 },
151 }
152 }
153 }
154 }
155
156 writeln!(
157 stdout,
158 "Successfully processed {} repositories",
159 repos_to_tag.len()
160 )?;
161 Ok(())
162}
163
164#[derive(Debug, Clone, PartialEq)]
165enum TagResult {
166 Created,
167 AlreadyExists,
168}
169
170fn create_tag(repo: &repo::Repo, tag_name: &str, sign: bool) -> Result<TagResult> {
171 if repo
173 .git_repo
174 .revparse_single(&format!("refs/tags/{}", tag_name))
175 .is_ok()
176 {
177 return Ok(TagResult::AlreadyExists);
178 }
179
180 let head = repo.git_repo.head()?;
182 let commit = head.peel_to_commit()?;
183 let commit_obj = commit.as_object();
184
185 if sign {
187 let signature = repo.git_repo.signature()?;
189 let _tag_ref = repo.git_repo.tag(
190 tag_name,
191 commit_obj,
192 &signature,
193 &format!("Tag {}", tag_name),
194 false,
195 )?;
196 } else {
197 let _tag_ref = repo.git_repo.tag_lightweight(tag_name, commit_obj, false)?;
199 }
200
201 Ok(TagResult::Created)
202}
203
204fn list_tags(repo: &repo::Repo) -> Result<Vec<String>> {
205 let mut tags = Vec::new();
206
207 let tag_names = repo.git_repo.tag_names(None)?;
209
210 for tag_name in tag_names.iter().flatten() {
211 tags.push(tag_name.to_string());
212 }
213
214 tags.sort();
216
217 Ok(tags)
218}
219
220fn push_tags(repo: &repo::Repo) -> Result<()> {
221 let head_ref = repo.git_repo.head()?;
223 let branch_name = head_ref.shorthand().with_context(|| {
224 format!(
225 "Cannot get branch name for repo at `{}`",
226 repo.work_dir.display()
227 )
228 })?;
229
230 let remote_name = repo.get_remote_name_for_branch(branch_name)?;
231
232 let mut remote = match repo.git_repo.find_remote(&remote_name) {
234 Ok(remote) => remote,
235 Err(_) => {
236 return Err(anyhow!("No remote '{}' configured", remote_name));
237 },
238 };
239
240 let refspecs = &["refs/tags/*:refs/tags/*"];
242 let mut push_options = git2::PushOptions::new();
243 push_options.remote_callbacks(git2::RemoteCallbacks::new());
244
245 remote.push(refspecs, Some(&mut push_options))?;
246
247 Ok(())
248}