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 push<W: Write>(
9 wok_config: &mut config::Config,
10 umbrella: &repo::Repo,
11 stdout: &mut W,
12 set_upstream: bool,
13 all: bool,
14 branch_name: Option<&str>,
15 include_umbrella: bool,
16 target_repos: &[std::path::PathBuf],
17) -> Result<()> {
18 let target_branch = match branch_name {
20 Some(name) => name.to_string(),
21 None => umbrella.head.clone(),
22 };
23
24 let repos_to_push: Vec<config::Repo> = if all {
26 wok_config
28 .repos
29 .iter()
30 .filter(|config_repo| {
31 !config_repo.is_skipped_for("push")
32 || target_repos.contains(&config_repo.path)
33 })
34 .cloned()
35 .collect()
36 } else if !target_repos.is_empty() {
37 wok_config
39 .repos
40 .iter()
41 .filter(|config_repo| target_repos.contains(&config_repo.path))
42 .cloned()
43 .collect()
44 } else {
45 wok_config
47 .repos
48 .iter()
49 .filter(|config_repo| config_repo.head == umbrella.head)
50 .cloned()
51 .collect()
52 };
53
54 let total_targets = repos_to_push.len() + usize::from(include_umbrella);
55
56 if total_targets == 0 {
57 writeln!(stdout, "No repositories to push")?;
58 return Ok(());
59 }
60
61 writeln!(
62 stdout,
63 "Pushing {} repositories to branch '{}'...",
64 total_targets, target_branch
65 )?;
66
67 if include_umbrella {
68 match push_repo(umbrella, &target_branch, set_upstream) {
69 Ok(result) => match result {
70 PushResult::Pushed => {
71 writeln!(stdout, "- 'umbrella': pushed to '{}'", target_branch)?;
72 },
73 PushResult::UpstreamSet => {
74 writeln!(
75 stdout,
76 "- 'umbrella': pushed to '{}' and set upstream",
77 target_branch
78 )?;
79 },
80 PushResult::UpToDate => {
81 writeln!(stdout, "- 'umbrella': already up to date")?;
82 },
83 PushResult::NoRemote => {
84 writeln!(stdout, "- 'umbrella': no remote configured, skipping")?;
85 },
86 },
87 Err(e) => {
88 writeln!(
89 stdout,
90 "- 'umbrella': failed to push to '{}' - {}",
91 target_branch, e
92 )?;
93 },
94 }
95 }
96
97 for config_repo in &repos_to_push {
99 if let Some(subrepo) = umbrella.get_subrepo_by_path(&config_repo.path) {
100 match push_repo(subrepo, &target_branch, set_upstream) {
101 Ok(result) => match result {
102 PushResult::Pushed => {
103 writeln!(
104 stdout,
105 "- '{}': pushed to '{}'",
106 config_repo.path.display(),
107 target_branch
108 )?;
109 },
110 PushResult::UpstreamSet => {
111 writeln!(
112 stdout,
113 "- '{}': pushed to '{}' and set upstream",
114 config_repo.path.display(),
115 target_branch
116 )?;
117 },
118 PushResult::UpToDate => {
119 writeln!(
120 stdout,
121 "- '{}': already up to date",
122 config_repo.path.display()
123 )?;
124 },
125 PushResult::NoRemote => {
126 writeln!(
127 stdout,
128 "- '{}': no remote configured, skipping",
129 config_repo.path.display()
130 )?;
131 },
132 },
133 Err(e) => {
134 writeln!(
135 stdout,
136 "- '{}': failed to push to '{}' - {}",
137 config_repo.path.display(),
138 target_branch,
139 e
140 )?;
141 },
142 }
143 }
144 }
145
146 writeln!(
147 stdout,
148 "Successfully processed {} repositories",
149 total_targets
150 )?;
151 Ok(())
152}
153
154#[derive(Debug, Clone, PartialEq)]
155enum PushResult {
156 Pushed,
157 UpstreamSet,
158 UpToDate,
159 NoRemote,
160}
161
162fn needs_push(
165 repo: &repo::Repo,
166 remote: &mut git2::Remote,
167 branch_name: &str,
168) -> Result<bool> {
169 let local_branch_ref = format!("refs/heads/{}", branch_name);
171 let local_oid = repo.git_repo.refname_to_id(&local_branch_ref)?;
172
173 let connection = remote.connect_auth(
175 git2::Direction::Push,
176 Some(repo.remote_callbacks()?),
177 None,
178 )?;
179
180 let remote_branch_ref = format!("refs/heads/{}", branch_name);
182 let mut remote_oid: Option<git2::Oid> = None;
183
184 for head in connection.list()?.iter() {
185 if head.name() == remote_branch_ref {
186 remote_oid = Some(head.oid());
187 break;
188 }
189 }
190
191 drop(connection);
192
193 let remote_oid = match remote_oid {
195 Some(oid) => oid,
196 None => return Ok(true), };
198
199 if local_oid == remote_oid {
201 return Ok(false);
202 }
203
204 Ok(true)
208}
209
210fn push_repo(
211 repo: &repo::Repo,
212 branch_name: &str,
213 set_upstream: bool,
214) -> Result<PushResult> {
215 let remote_name = repo.get_remote_name_for_branch(branch_name)?;
217
218 let mut remote = match repo.git_repo.find_remote(&remote_name) {
220 Ok(remote) => remote,
221 Err(_) => {
222 return Ok(PushResult::NoRemote);
223 },
224 };
225
226 let branch_ref = format!("refs/heads/{}", branch_name);
228
229 if repo.git_repo.refname_to_id(&branch_ref).is_err() {
231 return Err(anyhow!("Branch '{}' does not exist locally", branch_name));
232 }
233
234 match needs_push(repo, &mut remote, branch_name) {
236 Ok(false) => {
237 return Ok(PushResult::UpToDate);
239 },
240 Ok(true) => {
241 },
243 Err(e) => {
244 eprintln!("Warning: Could not check remote state: {}", e);
248 },
249 }
250
251 let refspec = format!("{}:refs/heads/{}", branch_ref, branch_name);
253
254 let mut push_options = git2::PushOptions::new();
256 push_options.remote_callbacks(repo.remote_callbacks()?);
257
258 match remote.push(&[&refspec], Some(&mut push_options)) {
259 Ok(_) => {
260 if set_upstream {
261 set_upstream_branch(repo, branch_name, &remote_name)?;
263 Ok(PushResult::UpstreamSet)
264 } else {
265 Ok(PushResult::Pushed)
266 }
267 },
268 Err(e) => {
269 if e.message().contains("up to date")
271 || e.message().contains("non-fast-forward")
272 {
273 Ok(PushResult::UpToDate)
274 } else {
275 Err(e.into())
276 }
277 },
278 }
279}
280
281fn set_upstream_branch(
282 repo: &repo::Repo,
283 branch_name: &str,
284 remote_name: &str,
285) -> Result<()> {
286 let mut config = repo.git_repo.config()?;
288 config.set_str(&format!("branch.{}.remote", branch_name), remote_name)?;
289 config.set_str(
290 &format!("branch.{}.merge", branch_name),
291 &format!("refs/heads/{}", branch_name),
292 )?;
293
294 Ok(())
295}