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
129/// Check if the local branch has new commits compared to the remote branch.
130/// Returns Ok(true) if push is needed, Ok(false) if already up-to-date, Err on failure.
131fn needs_push(
132    repo: &repo::Repo,
133    remote: &mut git2::Remote,
134    branch_name: &str,
135) -> Result<bool> {
136    // Get local branch OID
137    let local_branch_ref = format!("refs/heads/{}", branch_name);
138    let local_oid = repo.git_repo.refname_to_id(&local_branch_ref)?;
139
140    // Connect to remote to check remote branch state
141    let connection = remote.connect_auth(
142        git2::Direction::Push,
143        Some(repo.remote_callbacks()?),
144        None,
145    )?;
146
147    // Check if remote branch exists and get its OID
148    let remote_branch_ref = format!("refs/heads/{}", branch_name);
149    let mut remote_oid: Option<git2::Oid> = None;
150
151    for head in connection.list()?.iter() {
152        if head.name() == remote_branch_ref {
153            remote_oid = Some(head.oid());
154            break;
155        }
156    }
157
158    drop(connection);
159
160    // If remote branch doesn't exist, we need to push
161    let remote_oid = match remote_oid {
162        Some(oid) => oid,
163        None => return Ok(true), // New branch, push needed
164    };
165
166    // If OIDs match, no push needed
167    if local_oid == remote_oid {
168        return Ok(false);
169    }
170
171    // Check if local is ahead of remote (can fast-forward)
172    // If remote is ahead or diverged, we still return true because
173    // git push will fail with proper error message
174    Ok(true)
175}
176
177fn push_repo(
178    repo: &repo::Repo,
179    branch_name: &str,
180    set_upstream: bool,
181) -> Result<PushResult> {
182    // Get the remote name for this branch
183    let remote_name = repo.get_remote_name_for_branch(branch_name)?;
184
185    // Check if remote exists
186    let mut remote = match repo.git_repo.find_remote(&remote_name) {
187        Ok(remote) => remote,
188        Err(_) => {
189            return Ok(PushResult::NoRemote);
190        },
191    };
192
193    // Get the current branch reference
194    let branch_ref = format!("refs/heads/{}", branch_name);
195
196    // Check if the branch exists locally
197    if repo.git_repo.refname_to_id(&branch_ref).is_err() {
198        return Err(anyhow!("Branch '{}' does not exist locally", branch_name));
199    }
200
201    // Check if push is actually needed
202    match needs_push(repo, &mut remote, branch_name) {
203        Ok(false) => {
204            // Already up to date, skip the push entirely
205            return Ok(PushResult::UpToDate);
206        },
207        Ok(true) => {
208            // Push is needed, continue
209        },
210        Err(e) => {
211            // If we can't check remote state (e.g., network issue),
212            // proceed with push attempt and let git2 handle it
213            // This maintains backwards compatibility
214            eprintln!("Warning: Could not check remote state: {}", e);
215        },
216    }
217
218    // Prepare the refspec for pushing
219    let refspec = format!("{}:refs/heads/{}", branch_ref, branch_name);
220
221    // Perform the push
222    let mut push_options = git2::PushOptions::new();
223    push_options.remote_callbacks(repo.remote_callbacks()?);
224
225    match remote.push(&[&refspec], Some(&mut push_options)) {
226        Ok(_) => {
227            if set_upstream {
228                // Set the upstream branch
229                set_upstream_branch(repo, branch_name, &remote_name)?;
230                Ok(PushResult::UpstreamSet)
231            } else {
232                Ok(PushResult::Pushed)
233            }
234        },
235        Err(e) => {
236            // Check if it's an "up to date" error
237            if e.message().contains("up to date")
238                || e.message().contains("non-fast-forward")
239            {
240                Ok(PushResult::UpToDate)
241            } else {
242                Err(e.into())
243            }
244        },
245    }
246}
247
248fn set_upstream_branch(
249    repo: &repo::Repo,
250    branch_name: &str,
251    remote_name: &str,
252) -> Result<()> {
253    // Update the branch configuration to set upstream
254    let mut config = repo.git_repo.config()?;
255    config.set_str(&format!("branch.{}.remote", branch_name), remote_name)?;
256    config.set_str(
257        &format!("branch.{}.merge", branch_name),
258        &format!("refs/heads/{}", branch_name),
259    )?;
260
261    Ok(())
262}