git_wok/cmd/
push.rs

1use anyhow::*;
2use std::io::Write;
3use std::result::Result::Ok;
4
5use crate::{config, repo};
6
7pub fn push<W: Write>(
8    wok_config: &mut config::Config,
9    umbrella: &repo::Repo,
10    stdout: &mut W,
11    set_upstream: bool,
12    all: bool,
13    branch_name: Option<&str>,
14    target_repos: &[std::path::PathBuf],
15) -> Result<()> {
16    // Determine the target branch
17    let target_branch = match branch_name {
18        Some(name) => name.to_string(),
19        None => umbrella.head.clone(),
20    };
21
22    // Determine which repos to push
23    let repos_to_push: Vec<config::Repo> = if all {
24        // Push all configured repos, skipping those opted out unless explicitly targeted
25        wok_config
26            .repos
27            .iter()
28            .filter(|config_repo| {
29                !config_repo.is_skipped_for("push")
30                    || target_repos.contains(&config_repo.path)
31            })
32            .cloned()
33            .collect()
34    } else if !target_repos.is_empty() {
35        // Push only specified repos
36        wok_config
37            .repos
38            .iter()
39            .filter(|config_repo| target_repos.contains(&config_repo.path))
40            .cloned()
41            .collect()
42    } else {
43        // Push repos that match the current main repo branch
44        wok_config
45            .repos
46            .iter()
47            .filter(|config_repo| config_repo.head == umbrella.head)
48            .cloned()
49            .collect()
50    };
51
52    if repos_to_push.is_empty() {
53        writeln!(stdout, "No repositories to push")?;
54        return Ok(());
55    }
56
57    writeln!(
58        stdout,
59        "Pushing {} repositories to branch '{}'...",
60        repos_to_push.len(),
61        target_branch
62    )?;
63
64    // Push each repo
65    for config_repo in &repos_to_push {
66        if let Some(subrepo) = umbrella.get_subrepo_by_path(&config_repo.path) {
67            match push_repo(subrepo, &target_branch, set_upstream) {
68                Ok(result) => match result {
69                    PushResult::Pushed => {
70                        writeln!(
71                            stdout,
72                            "- '{}': pushed to '{}'",
73                            config_repo.path.display(),
74                            target_branch
75                        )?;
76                    },
77                    PushResult::UpstreamSet => {
78                        writeln!(
79                            stdout,
80                            "- '{}': pushed to '{}' and set upstream",
81                            config_repo.path.display(),
82                            target_branch
83                        )?;
84                    },
85                    PushResult::UpToDate => {
86                        writeln!(
87                            stdout,
88                            "- '{}': already up to date",
89                            config_repo.path.display()
90                        )?;
91                    },
92                    PushResult::NoRemote => {
93                        writeln!(
94                            stdout,
95                            "- '{}': no remote configured, skipping",
96                            config_repo.path.display()
97                        )?;
98                    },
99                },
100                Err(e) => {
101                    writeln!(
102                        stdout,
103                        "- '{}': failed to push to '{}' - {}",
104                        config_repo.path.display(),
105                        target_branch,
106                        e
107                    )?;
108                },
109            }
110        }
111    }
112
113    writeln!(
114        stdout,
115        "Successfully processed {} repositories",
116        repos_to_push.len()
117    )?;
118    Ok(())
119}
120
121#[derive(Debug, Clone, PartialEq)]
122enum PushResult {
123    Pushed,
124    UpstreamSet,
125    UpToDate,
126    NoRemote,
127}
128
129fn push_repo(
130    repo: &repo::Repo,
131    branch_name: &str,
132    set_upstream: bool,
133) -> Result<PushResult> {
134    // Get the remote name for this branch
135    let remote_name = repo.get_remote_name_for_branch(branch_name)?;
136
137    // Check if remote exists
138    let mut remote = match repo.git_repo.find_remote(&remote_name) {
139        Ok(remote) => remote,
140        Err(_) => {
141            return Ok(PushResult::NoRemote);
142        },
143    };
144
145    // Get the current branch reference
146    let branch_ref = format!("refs/heads/{}", branch_name);
147
148    // Check if the branch exists locally
149    if repo.git_repo.refname_to_id(&branch_ref).is_err() {
150        return Err(anyhow!("Branch '{}' does not exist locally", branch_name));
151    }
152
153    // Prepare the refspec for pushing
154    let refspec = format!("{}:refs/heads/{}", branch_ref, branch_name);
155
156    // Perform the push
157    let mut push_options = git2::PushOptions::new();
158    push_options.remote_callbacks(repo.remote_callbacks()?);
159
160    match remote.push(&[&refspec], Some(&mut push_options)) {
161        Ok(_) => {
162            if set_upstream {
163                // Set the upstream branch
164                set_upstream_branch(repo, branch_name, &remote_name)?;
165                Ok(PushResult::UpstreamSet)
166            } else {
167                Ok(PushResult::Pushed)
168            }
169        },
170        Err(e) => {
171            // Check if it's an "up to date" error
172            if e.message().contains("up to date")
173                || e.message().contains("non-fast-forward")
174            {
175                Ok(PushResult::UpToDate)
176            } else {
177                Err(e.into())
178            }
179        },
180    }
181}
182
183fn set_upstream_branch(
184    repo: &repo::Repo,
185    branch_name: &str,
186    remote_name: &str,
187) -> Result<()> {
188    // Update the branch configuration to set upstream
189    let mut config = repo.git_repo.config()?;
190    config.set_str(&format!("branch.{}.remote", branch_name), remote_name)?;
191    config.set_str(
192        &format!("branch.{}.merge", branch_name),
193        &format!("refs/heads/{}", branch_name),
194    )?;
195
196    Ok(())
197}