wok_dev/cmd/
tag.rs

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    // Determine which repos to tag
19    let repos_to_tag: Vec<config::Repo> = if all {
20        // Tag all configured repos, skipping those opted out unless explicitly targeted
21        wok_config
22            .repos
23            .iter()
24            .filter(|config_repo| {
25                !config_repo.is_skipped_for("tag")
26                    || target_repos.contains(&config_repo.path)
27            })
28            .cloned()
29            .collect()
30    } else if !target_repos.is_empty() {
31        // Tag only specified repos
32        wok_config
33            .repos
34            .iter()
35            .filter(|config_repo| target_repos.contains(&config_repo.path))
36            .cloned()
37            .collect()
38    } else {
39        // Tag repos that match the current main repo branch
40        wok_config
41            .repos
42            .iter()
43            .filter(|config_repo| config_repo.head == umbrella.head)
44            .cloned()
45            .collect()
46    };
47
48    if repos_to_tag.is_empty() {
49        writeln!(stdout, "No repositories to tag")?;
50        return Ok(());
51    }
52
53    match tag_name {
54        Some(name) => {
55            // Create new tag
56            writeln!(
57                stdout,
58                "Creating tag '{}' in {} repositories...",
59                name,
60                repos_to_tag.len()
61            )?;
62
63            for config_repo in &repos_to_tag {
64                if let Some(subrepo) = umbrella.get_subrepo_by_path(&config_repo.path) {
65                    match create_tag(subrepo, name, sign) {
66                        Ok(result) => match result {
67                            TagResult::Created => {
68                                writeln!(
69                                    stdout,
70                                    "- '{}': created tag '{}'",
71                                    config_repo.path.display(),
72                                    name
73                                )?;
74                            },
75                            TagResult::AlreadyExists => {
76                                writeln!(
77                                    stdout,
78                                    "- '{}': tag '{}' already exists",
79                                    config_repo.path.display(),
80                                    name
81                                )?;
82                            },
83                        },
84                        Err(e) => {
85                            writeln!(
86                                stdout,
87                                "- '{}': failed to create tag '{}' - {}",
88                                config_repo.path.display(),
89                                name,
90                                e
91                            )?;
92                        },
93                    }
94                }
95            }
96        },
97        None => {
98            // List existing tags
99            writeln!(
100                stdout,
101                "Listing tags in {} repositories...",
102                repos_to_tag.len()
103            )?;
104
105            for config_repo in &repos_to_tag {
106                if let Some(subrepo) = umbrella.get_subrepo_by_path(&config_repo.path) {
107                    match list_tags(subrepo) {
108                        Ok(tags) => {
109                            if tags.is_empty() {
110                                writeln!(
111                                    stdout,
112                                    "- '{}': no tags found",
113                                    config_repo.path.display()
114                                )?;
115                            } else {
116                                writeln!(
117                                    stdout,
118                                    "- '{}': {}",
119                                    config_repo.path.display(),
120                                    tags.join(", ")
121                                )?;
122                            }
123                        },
124                        Err(e) => {
125                            writeln!(
126                                stdout,
127                                "- '{}': failed to list tags - {}",
128                                config_repo.path.display(),
129                                e
130                            )?;
131                        },
132                    }
133                }
134            }
135        },
136    }
137
138    // Push tags if requested
139    if push {
140        writeln!(stdout, "Pushing tags to remotes...")?;
141        for config_repo in &repos_to_tag {
142            if let Some(subrepo) = umbrella.get_subrepo_by_path(&config_repo.path) {
143                match push_tags(subrepo) {
144                    Ok(_) => {
145                        writeln!(
146                            stdout,
147                            "- '{}': pushed tags",
148                            config_repo.path.display()
149                        )?;
150                    },
151                    Err(e) => {
152                        writeln!(
153                            stdout,
154                            "- '{}': failed to push tags - {}",
155                            config_repo.path.display(),
156                            e
157                        )?;
158                    },
159                }
160            }
161        }
162    }
163
164    writeln!(
165        stdout,
166        "Successfully processed {} repositories",
167        repos_to_tag.len()
168    )?;
169    Ok(())
170}
171
172#[derive(Debug, Clone, PartialEq)]
173enum TagResult {
174    Created,
175    AlreadyExists,
176}
177
178fn create_tag(repo: &repo::Repo, tag_name: &str, sign: bool) -> Result<TagResult> {
179    // Check if tag already exists by trying to find it
180    if repo
181        .git_repo
182        .revparse_single(&format!("refs/tags/{}", tag_name))
183        .is_ok()
184    {
185        return Ok(TagResult::AlreadyExists);
186    }
187
188    // Get the current HEAD commit
189    let head = repo.git_repo.head()?;
190    let commit = head.peel_to_commit()?;
191    let commit_obj = commit.as_object();
192
193    // Create the tag
194    if sign {
195        // Create signed tag
196        let signature = repo.git_repo.signature()?;
197        let _tag_ref = repo.git_repo.tag(
198            tag_name,
199            commit_obj,
200            &signature,
201            &format!("Tag {}", tag_name),
202            false,
203        )?;
204    } else {
205        // Create lightweight tag
206        let _tag_ref = repo.git_repo.tag_lightweight(tag_name, commit_obj, false)?;
207    }
208
209    Ok(TagResult::Created)
210}
211
212fn list_tags(repo: &repo::Repo) -> Result<Vec<String>> {
213    let mut tags = Vec::new();
214
215    // Get all tag references
216    let tag_names = repo.git_repo.tag_names(None)?;
217
218    for tag_name in tag_names.iter().flatten() {
219        tags.push(tag_name.to_string());
220    }
221
222    // Sort tags for consistent output
223    tags.sort();
224
225    Ok(tags)
226}
227
228fn push_tags(repo: &repo::Repo) -> Result<()> {
229    // Get the remote name for the current branch
230    let head_ref = repo.git_repo.head()?;
231    let branch_name = head_ref.shorthand().with_context(|| {
232        format!(
233            "Cannot get branch name for repo at `{}`",
234            repo.work_dir.display()
235        )
236    })?;
237
238    let remote_name = repo.get_remote_name_for_branch(branch_name)?;
239
240    // Check if remote exists
241    let mut remote = match repo.git_repo.find_remote(&remote_name) {
242        Ok(remote) => remote,
243        Err(_) => {
244            return Err(anyhow!("No remote '{}' configured", remote_name));
245        },
246    };
247
248    // Push all tags
249    let refspecs = &["refs/tags/*:refs/tags/*"];
250    let mut push_options = git2::PushOptions::new();
251    push_options.remote_callbacks(repo.remote_callbacks()?);
252
253    remote.push(refspecs, Some(&mut push_options))?;
254
255    Ok(())
256}