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 for config_repo in &repos_to_push {
70 if let Some(subrepo) = umbrella.get_subrepo_by_path(&config_repo.path) {
71 match push_repo(subrepo, &target_branch, set_upstream) {
72 Ok(result) => match result {
73 PushResult::Pushed => {
74 writeln!(
75 stdout,
76 "- '{}': pushed to '{}'",
77 config_repo.path.display(),
78 target_branch
79 )?;
80 },
81 PushResult::UpstreamSet => {
82 writeln!(
83 stdout,
84 "- '{}': pushed to '{}' and set upstream",
85 config_repo.path.display(),
86 target_branch
87 )?;
88 },
89 PushResult::UpToDate => {
90 writeln!(
91 stdout,
92 "- '{}': already up to date",
93 config_repo.path.display()
94 )?;
95 },
96 PushResult::NoRemote => {
97 writeln!(
98 stdout,
99 "- '{}': no remote configured, skipping",
100 config_repo.path.display()
101 )?;
102 },
103 },
104 Err(e) => {
105 writeln!(
106 stdout,
107 "- '{}': failed to push to '{}' - {}",
108 config_repo.path.display(),
109 target_branch,
110 e
111 )?;
112 },
113 }
114 }
115 }
116
117 if include_umbrella {
119 match push_repo(umbrella, &target_branch, set_upstream) {
120 Ok(result) => match result {
121 PushResult::Pushed => {
122 writeln!(stdout, "- 'umbrella': pushed to '{}'", target_branch)?;
123 },
124 PushResult::UpstreamSet => {
125 writeln!(
126 stdout,
127 "- 'umbrella': pushed to '{}' and set upstream",
128 target_branch
129 )?;
130 },
131 PushResult::UpToDate => {
132 writeln!(stdout, "- 'umbrella': already up to date")?;
133 },
134 PushResult::NoRemote => {
135 writeln!(stdout, "- 'umbrella': no remote configured, skipping")?;
136 },
137 },
138 Err(e) => {
139 writeln!(
140 stdout,
141 "- 'umbrella': failed to push to '{}' - {}",
142 target_branch, e
143 )?;
144 },
145 }
146 }
147
148 writeln!(
149 stdout,
150 "Successfully processed {} repositories",
151 total_targets
152 )?;
153 Ok(())
154}
155
156#[derive(Debug, Clone, PartialEq)]
157enum PushResult {
158 Pushed,
159 UpstreamSet,
160 UpToDate,
161 NoRemote,
162}
163
164fn needs_push(
167 repo: &repo::Repo,
168 remote: &mut git2::Remote,
169 branch_name: &str,
170) -> Result<bool> {
171 let local_branch_ref = format!("refs/heads/{}", branch_name);
173 let local_oid = repo.git_repo.refname_to_id(&local_branch_ref)?;
174
175 let connection = remote.connect_auth(
177 git2::Direction::Push,
178 Some(repo.remote_callbacks()?),
179 None,
180 )?;
181
182 let remote_branch_ref = format!("refs/heads/{}", branch_name);
184 let mut remote_oid: Option<git2::Oid> = None;
185
186 for head in connection.list()?.iter() {
187 if head.name() == remote_branch_ref {
188 remote_oid = Some(head.oid());
189 break;
190 }
191 }
192
193 drop(connection);
194
195 let remote_oid = match remote_oid {
197 Some(oid) => oid,
198 None => return Ok(true), };
200
201 if local_oid == remote_oid {
203 return Ok(false);
204 }
205
206 Ok(true)
210}
211
212fn push_repo(
213 repo: &repo::Repo,
214 branch_name: &str,
215 set_upstream: bool,
216) -> Result<PushResult> {
217 let remote_name = repo.get_remote_name_for_branch(branch_name)?;
219
220 let mut remote = match repo.git_repo.find_remote(&remote_name) {
222 Ok(remote) => remote,
223 Err(_) => {
224 return Ok(PushResult::NoRemote);
225 },
226 };
227
228 let branch_ref = format!("refs/heads/{}", branch_name);
230
231 if repo.git_repo.refname_to_id(&branch_ref).is_err() {
233 return Err(anyhow!("Branch '{}' does not exist locally", branch_name));
234 }
235
236 match needs_push(repo, &mut remote, branch_name) {
238 Ok(false) => {
239 return Ok(PushResult::UpToDate);
241 },
242 Ok(true) => {
243 },
245 Err(e) => {
246 eprintln!("Warning: Could not check remote state: {}", e);
250 },
251 }
252
253 let refspec = format!("{}:refs/heads/{}", branch_ref, branch_name);
255
256 let mut push_options = git2::PushOptions::new();
258 push_options.remote_callbacks(repo.remote_callbacks()?);
259
260 match remote.push(&[&refspec], Some(&mut push_options)) {
261 Ok(_) => {
262 if set_upstream {
263 set_upstream_branch(repo, branch_name, &remote_name)?;
265 Ok(PushResult::UpstreamSet)
266 } else {
267 Ok(PushResult::Pushed)
268 }
269 },
270 Err(e) => {
271 if e.message().contains("up to date")
273 || e.message().contains("non-fast-forward")
274 {
275 Ok(PushResult::UpToDate)
276 } else {
277 Err(e.into())
278 }
279 },
280 }
281}
282
283fn set_upstream_branch(
284 repo: &repo::Repo,
285 branch_name: &str,
286 remote_name: &str,
287) -> Result<()> {
288 let mut config = repo.git_repo.config()?;
290 config.set_str(&format!("branch.{}.remote", branch_name), remote_name)?;
291 config.set_str(
292 &format!("branch.{}.merge", branch_name),
293 &format!("refs/heads/{}", branch_name),
294 )?;
295
296 Ok(())
297}