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 = if all {
20        // Tag all configured repos
21        wok_config.repos.clone()
22    } else if !target_repos.is_empty() {
23        // Tag only specified repos
24        wok_config
25            .repos
26            .iter()
27            .filter(|config_repo| target_repos.contains(&config_repo.path))
28            .cloned()
29            .collect()
30    } else {
31        // Tag repos that match the current main repo branch
32        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            // Create new tag
48            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            // List existing tags
91            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    // Push tags if requested
131    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    // Check if tag already exists by trying to find it
172    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    // Get the current HEAD commit
181    let head = repo.git_repo.head()?;
182    let commit = head.peel_to_commit()?;
183    let commit_obj = commit.as_object();
184
185    // Create the tag
186    if sign {
187        // Create signed tag
188        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        // Create lightweight tag
198        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    // Get all tag references
208    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    // Sort tags for consistent output
215    tags.sort();
216
217    Ok(tags)
218}
219
220fn push_tags(repo: &repo::Repo) -> Result<()> {
221    // Get the remote name for the current branch
222    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    // Check if remote exists
233    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    // Push all tags
241    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}