1use crossbeam_channel::Sender;
2use git2::{PackBuilderStage, PushOptions};
3use scopetime::scope_time;
4
5use crate::{
6 error::{Error, Result},
7 progress::ProgressPercent,
8 sync::{
9 branch::branch_set_upstream_after_push,
10 cred::BasicAuthCredential,
11 remotes::{proxy_auto, Callbacks},
12 repository::repo,
13 CommitId, RepoPath,
14 },
15};
16
17pub trait AsyncProgress: Clone + Send + Sync {
19 fn is_done(&self) -> bool;
21 fn progress(&self) -> ProgressPercent;
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum ProgressNotification {
28 UpdateTips {
30 name: String,
32 a: CommitId,
34 b: CommitId,
36 },
37 Transfer {
39 objects: usize,
41 total_objects: usize,
43 },
44 PushTransfer {
46 current: usize,
48 total: usize,
50 bytes: usize,
52 },
53 Packing {
55 stage: PackBuilderStage,
57 total: usize,
59 current: usize,
61 },
62 Done,
64}
65
66impl AsyncProgress for ProgressNotification {
67 fn is_done(&self) -> bool {
68 *self == Self::Done
69 }
70 fn progress(&self) -> ProgressPercent {
71 match *self {
72 Self::Packing {
73 stage,
74 current,
75 total,
76 } => match stage {
77 PackBuilderStage::AddingObjects | PackBuilderStage::Deltafication => {
78 ProgressPercent::new(current, total)
79 }
80 },
81 Self::PushTransfer { current, total, .. } => ProgressPercent::new(current, total),
82 Self::Transfer {
83 objects,
84 total_objects,
85 ..
86 } => ProgressPercent::new(objects, total_objects),
87 _ => ProgressPercent::full(),
88 }
89 }
90}
91
92#[derive(Copy, Clone, Debug)]
94pub enum PushType {
95 Branch,
97 Tag,
99}
100
101impl Default for PushType {
102 fn default() -> Self {
103 Self::Branch
104 }
105}
106
107#[cfg(test)]
108pub fn push_branch(
109 repo_path: &RepoPath,
110 remote: &str,
111 branch: &str,
112 force: bool,
113 delete: bool,
114 basic_credential: Option<BasicAuthCredential>,
115 progress_sender: Option<Sender<ProgressNotification>>,
116) -> Result<()> {
117 push_raw(
118 repo_path,
119 remote,
120 branch,
121 PushType::Branch,
122 force,
123 delete,
124 basic_credential,
125 progress_sender,
126 )
127}
128
129#[allow(clippy::too_many_arguments)]
131pub fn push_raw(
132 repo_path: &RepoPath,
133 remote: &str,
134 branch: &str,
135 ref_type: PushType,
136 force: bool,
137 delete: bool,
138 basic_credential: Option<BasicAuthCredential>,
139 progress_sender: Option<Sender<ProgressNotification>>,
140) -> Result<()> {
141 scope_time!("push");
142
143 let repo = repo(repo_path)?;
144 let mut remote = repo.find_remote(remote)?;
145
146 let mut options = PushOptions::new();
147 options.proxy_options(proxy_auto());
148
149 let callbacks = Callbacks::new(progress_sender, basic_credential);
150 options.remote_callbacks(callbacks.callbacks());
151 options.packbuilder_parallelism(0);
152
153 let branch_modifier = match (force, delete) {
154 (true, true) => "+:",
155 (false, true) => ":",
156 (true, false) => "+",
157 (false, false) => "",
158 };
159 let ref_type = match ref_type {
160 PushType::Branch => "heads",
161 PushType::Tag => "tags",
162 };
163
164 let branch_name = format!("{branch_modifier}refs/{ref_type}/{branch}");
165 remote.push(&[branch_name.as_str()], Some(&mut options))?;
166
167 if let Some((reference, msg)) = callbacks.get_stats()?.push_rejected_msg {
168 return Err(Error::Generic(format!(
169 "push to '{reference}' rejected: {msg}"
170 )));
171 }
172
173 if !delete {
174 branch_set_upstream_after_push(&repo, branch)?;
175 }
176
177 Ok(())
178}
179
180#[cfg(test)]
181mod tests {
182 use std::{fs::File, io::Write, path::Path};
183
184 use git2::Repository;
185
186 use super::*;
187 use crate::sync::{
188 self,
189 tests::{get_commit_ids, repo_clone, repo_init, repo_init_bare, write_commit_file},
190 };
191
192 #[test]
193 fn test_force_push() {
194 let (tmp_repo_dir, repo) = repo_init().unwrap();
198 let (tmp_other_repo_dir, other_repo) = repo_init().unwrap();
199 let (tmp_upstream_dir, _) = repo_init_bare().unwrap();
200
201 repo.remote("origin", tmp_upstream_dir.path().to_str().unwrap())
202 .unwrap();
203
204 other_repo
205 .remote("origin", tmp_upstream_dir.path().to_str().unwrap())
206 .unwrap();
207
208 let tmp_repo_file_path = tmp_repo_dir.path().join("temp_file.txt");
209 let mut tmp_repo_file = File::create(tmp_repo_file_path).unwrap();
210 writeln!(tmp_repo_file, "TempSomething").unwrap();
211
212 sync::commit(
213 &tmp_repo_dir.path().to_str().unwrap().into(),
214 "repo_1_commit",
215 )
216 .unwrap();
217
218 push_branch(
219 &tmp_repo_dir.path().to_str().unwrap().into(),
220 "origin",
221 "master",
222 false,
223 false,
224 None,
225 None,
226 )
227 .unwrap();
228
229 let tmp_other_repo_file_path = tmp_other_repo_dir.path().join("temp_file.txt");
230 let mut tmp_other_repo_file = File::create(tmp_other_repo_file_path).unwrap();
231 writeln!(tmp_other_repo_file, "TempElse").unwrap();
232
233 sync::commit(
234 &tmp_other_repo_dir.path().to_str().unwrap().into(),
235 "repo_2_commit",
236 )
237 .unwrap();
238
239 assert_eq!(
242 push_branch(
243 &tmp_other_repo_dir.path().to_str().unwrap().into(),
244 "origin",
245 "master",
246 false,
247 false,
248 None,
249 None,
250 )
251 .is_err(),
252 true
253 );
254
255 assert_eq!(
258 push_branch(
259 &tmp_other_repo_dir.path().to_str().unwrap().into(),
260 "origin",
261 "master",
262 true,
263 false,
264 None,
265 None,
266 )
267 .is_err(),
268 false
269 );
270 }
271
272 #[test]
273 fn test_force_push_rewrites_history() {
274 let (tmp_repo_dir, repo) = repo_init().unwrap();
279 let (tmp_other_repo_dir, other_repo) = repo_init().unwrap();
280 let (tmp_upstream_dir, upstream) = repo_init_bare().unwrap();
281
282 repo.remote("origin", tmp_upstream_dir.path().to_str().unwrap())
283 .unwrap();
284
285 other_repo
286 .remote("origin", tmp_upstream_dir.path().to_str().unwrap())
287 .unwrap();
288
289 let tmp_repo_file_path = tmp_repo_dir.path().join("temp_file.txt");
290 let mut tmp_repo_file = File::create(tmp_repo_file_path).unwrap();
291 writeln!(tmp_repo_file, "TempSomething").unwrap();
292
293 sync::stage_add_file(
294 &tmp_repo_dir.path().to_str().unwrap().into(),
295 Path::new("temp_file.txt"),
296 )
297 .unwrap();
298
299 let repo_1_commit = sync::commit(
300 &tmp_repo_dir.path().to_str().unwrap().into(),
301 "repo_1_commit",
302 )
303 .unwrap();
304
305 assert_eq!(
307 sync::get_commit_files(
308 &tmp_repo_dir.path().to_str().unwrap().into(),
309 repo_1_commit,
310 None
311 )
312 .unwrap()[0]
313 .path,
314 String::from("temp_file.txt")
315 );
316
317 let commits = get_commit_ids(&repo, 1);
318 assert!(commits.contains(&repo_1_commit));
319
320 push_branch(
321 &tmp_repo_dir.path().to_str().unwrap().into(),
322 "origin",
323 "master",
324 false,
325 false,
326 None,
327 None,
328 )
329 .unwrap();
330
331 let tmp_other_repo_file_path = tmp_other_repo_dir.path().join("temp_file.txt");
332 let mut tmp_other_repo_file = File::create(tmp_other_repo_file_path).unwrap();
333 writeln!(tmp_other_repo_file, "TempElse").unwrap();
334
335 sync::stage_add_file(
336 &tmp_other_repo_dir.path().to_str().unwrap().into(),
337 Path::new("temp_file.txt"),
338 )
339 .unwrap();
340
341 let repo_2_commit = sync::commit(
342 &tmp_other_repo_dir.path().to_str().unwrap().into(),
343 "repo_2_commit",
344 )
345 .unwrap();
346
347 let repo_2_parent = other_repo
348 .find_commit(repo_2_commit.into())
349 .unwrap()
350 .parents()
351 .next()
352 .unwrap()
353 .id();
354
355 let commits = get_commit_ids(&other_repo, 1);
356 assert!(commits.contains(&repo_2_commit));
357
358 assert_eq!(
361 push_branch(
362 &tmp_other_repo_dir.path().to_str().unwrap().into(),
363 "origin",
364 "master",
365 false,
366 false,
367 None,
368 None,
369 )
370 .is_err(),
371 true
372 );
373
374 let commits = get_commit_ids(&upstream, 1);
377 assert!(!commits.contains(&repo_2_commit));
378
379 push_branch(
383 &tmp_other_repo_dir.path().to_str().unwrap().into(),
384 "origin",
385 "master",
386 true,
387 false,
388 None,
389 None,
390 )
391 .unwrap();
392
393 let commits = get_commit_ids(&upstream, 1);
394 assert!(commits.contains(&repo_2_commit));
395
396 let new_upstream_parent = Repository::init_bare(tmp_upstream_dir.path())
397 .unwrap()
398 .find_commit(repo_2_commit.into())
399 .unwrap()
400 .parents()
401 .next()
402 .unwrap()
403 .id();
404 assert_eq!(new_upstream_parent, repo_2_parent,);
405 }
406
407 #[test]
408 fn test_delete_remote_branch() {
409 let (upstream_dir, upstream_repo) = repo_init_bare().unwrap();
413
414 let (tmp_repo_dir, repo) = repo_clone(upstream_dir.path().to_str().unwrap()).unwrap();
415
416 let commit_1 = write_commit_file(&repo, "temp_file.txt", "SomeContent", "Initial commit");
418
419 let commits = get_commit_ids(&repo, 1);
420 assert!(commits.contains(&commit_1));
421
422 push_branch(
423 &tmp_repo_dir.path().to_str().unwrap().into(),
424 "origin",
425 "master",
426 false,
427 false,
428 None,
429 None,
430 )
431 .unwrap();
432
433 sync::create_branch(&tmp_repo_dir.path().to_str().unwrap().into(), "test_branch").unwrap();
435
436 push_branch(
438 &tmp_repo_dir.path().to_str().unwrap().into(),
439 "origin",
440 "test_branch",
441 false,
442 false,
443 None,
444 None,
445 )
446 .unwrap();
447
448 assert_eq!(
450 upstream_repo
451 .branches(None)
452 .unwrap()
453 .map(std::result::Result::unwrap)
454 .map(|(i, _)| i.name().unwrap().unwrap().to_string())
455 .any(|i| &i == "test_branch"),
456 true
457 );
458
459 assert_eq!(
461 push_branch(
462 &tmp_repo_dir.path().to_str().unwrap().into(),
463 "origin",
464 "test_branch",
465 false,
466 true,
467 None,
468 None,
469 )
470 .is_ok(),
471 true
472 );
473
474 assert_eq!(
476 upstream_repo
477 .branches(None)
478 .unwrap()
479 .map(std::result::Result::unwrap)
480 .map(|(i, _)| i.name().unwrap().unwrap().to_string())
481 .any(|i| &i == "test_branch"),
482 false
483 );
484 }
485}