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 let target_branch = match branch_name {
18 Some(name) => name.to_string(),
19 None => umbrella.head.clone(),
20 };
21
22 let repos_to_push: Vec<config::Repo> = if all {
24 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 wok_config
37 .repos
38 .iter()
39 .filter(|config_repo| target_repos.contains(&config_repo.path))
40 .cloned()
41 .collect()
42 } else {
43 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 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 needs_push(
132 repo: &repo::Repo,
133 remote: &mut git2::Remote,
134 branch_name: &str,
135) -> Result<bool> {
136 let local_branch_ref = format!("refs/heads/{}", branch_name);
138 let local_oid = repo.git_repo.refname_to_id(&local_branch_ref)?;
139
140 let connection = remote.connect_auth(
142 git2::Direction::Push,
143 Some(repo.remote_callbacks()?),
144 None,
145 )?;
146
147 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 let remote_oid = match remote_oid {
162 Some(oid) => oid,
163 None => return Ok(true), };
165
166 if local_oid == remote_oid {
168 return Ok(false);
169 }
170
171 Ok(true)
175}
176
177fn push_repo(
178 repo: &repo::Repo,
179 branch_name: &str,
180 set_upstream: bool,
181) -> Result<PushResult> {
182 let remote_name = repo.get_remote_name_for_branch(branch_name)?;
184
185 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 let branch_ref = format!("refs/heads/{}", branch_name);
195
196 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 match needs_push(repo, &mut remote, branch_name) {
203 Ok(false) => {
204 return Ok(PushResult::UpToDate);
206 },
207 Ok(true) => {
208 },
210 Err(e) => {
211 eprintln!("Warning: Could not check remote state: {}", e);
215 },
216 }
217
218 let refspec = format!("{}:refs/heads/{}", branch_ref, branch_name);
220
221 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_upstream_branch(repo, branch_name, &remote_name)?;
230 Ok(PushResult::UpstreamSet)
231 } else {
232 Ok(PushResult::Pushed)
233 }
234 },
235 Err(e) => {
236 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 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}