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 CommitId, RepoPath,
10 branch::branch_set_upstream_after_push,
11 cred::BasicAuthCredential,
12 remotes::{Callbacks, proxy_auto},
13 repository::repo,
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
78 | PackBuilderStage::Deltafication => {
79 ProgressPercent::new(current, total)
80 }
81 },
82 Self::PushTransfer { current, total, .. } => {
83 ProgressPercent::new(current, total)
84 }
85 Self::Transfer {
86 objects,
87 total_objects,
88 ..
89 } => ProgressPercent::new(objects, total_objects),
90 _ => ProgressPercent::full(),
91 }
92 }
93}
94
95#[derive(Copy, Clone, Debug)]
97pub enum PushType {
98 Branch,
100 Tag,
102}
103
104impl Default for PushType {
105 fn default() -> Self {
106 Self::Branch
107 }
108}
109
110#[cfg(test)]
111pub fn push_branch(
112 repo_path: &RepoPath,
113 remote: &str,
114 branch: &str,
115 force: bool,
116 delete: bool,
117 basic_credential: Option<BasicAuthCredential>,
118 progress_sender: Option<Sender<ProgressNotification>>,
119) -> Result<()> {
120 push_raw(
121 repo_path,
122 remote,
123 branch,
124 PushType::Branch,
125 force,
126 delete,
127 basic_credential,
128 progress_sender,
129 )
130}
131
132#[allow(clippy::too_many_arguments)]
134pub fn push_raw(
135 repo_path: &RepoPath,
136 remote: &str,
137 branch: &str,
138 ref_type: PushType,
139 force: bool,
140 delete: bool,
141 basic_credential: Option<BasicAuthCredential>,
142 progress_sender: Option<Sender<ProgressNotification>>,
143) -> Result<()> {
144 scope_time!("push");
145
146 let repo = repo(repo_path)?;
147 let mut remote = repo.find_remote(remote)?;
148
149 let mut options = PushOptions::new();
150 options.proxy_options(proxy_auto());
151
152 let callbacks = Callbacks::new(progress_sender, basic_credential);
153 options.remote_callbacks(callbacks.callbacks());
154 options.packbuilder_parallelism(0);
155
156 let branch_modifier = match (force, delete) {
157 (true, true) => "+:",
158 (false, true) => ":",
159 (true, false) => "+",
160 (false, false) => "",
161 };
162 let ref_type = match ref_type {
163 PushType::Branch => "heads",
164 PushType::Tag => "tags",
165 };
166
167 let branch_name =
168 format!("{branch_modifier}refs/{ref_type}/{branch}");
169 remote.push(&[branch_name.as_str()], Some(&mut options))?;
170
171 if let Some((reference, msg)) =
172 callbacks.get_stats()?.push_rejected_msg
173 {
174 return Err(Error::Generic(format!(
175 "push to '{reference}' rejected: {msg}"
176 )));
177 }
178
179 if !delete {
180 branch_set_upstream_after_push(&repo, branch)?;
181 }
182
183 Ok(())
184}
185
186#[cfg(test)]
187mod tests {
188 use std::{fs::File, io::Write, path::Path};
189
190 use git2::Repository;
191
192 use super::*;
193 use crate::sync::{
194 self,
195 tests::{
196 get_commit_ids, repo_clone, repo_init, repo_init_bare,
197 write_commit_file,
198 },
199 };
200
201 #[test]
202 fn test_force_push() {
203 let (tmp_repo_dir, repo) = repo_init().unwrap();
207 let (tmp_other_repo_dir, other_repo) = repo_init().unwrap();
208 let (tmp_upstream_dir, _) = repo_init_bare().unwrap();
209
210 repo.remote(
211 "origin",
212 tmp_upstream_dir.path().to_str().unwrap(),
213 )
214 .unwrap();
215
216 other_repo
217 .remote(
218 "origin",
219 tmp_upstream_dir.path().to_str().unwrap(),
220 )
221 .unwrap();
222
223 let tmp_repo_file_path =
224 tmp_repo_dir.path().join("temp_file.txt");
225 let mut tmp_repo_file =
226 File::create(tmp_repo_file_path).unwrap();
227 writeln!(tmp_repo_file, "TempSomething").unwrap();
228
229 sync::commit(
230 &tmp_repo_dir.path().to_str().unwrap().into(),
231 "repo_1_commit",
232 )
233 .unwrap();
234
235 push_branch(
236 &tmp_repo_dir.path().to_str().unwrap().into(),
237 "origin",
238 "master",
239 false,
240 false,
241 None,
242 None,
243 )
244 .unwrap();
245
246 let tmp_other_repo_file_path =
247 tmp_other_repo_dir.path().join("temp_file.txt");
248 let mut tmp_other_repo_file =
249 File::create(tmp_other_repo_file_path).unwrap();
250 writeln!(tmp_other_repo_file, "TempElse").unwrap();
251
252 sync::commit(
253 &tmp_other_repo_dir.path().to_str().unwrap().into(),
254 "repo_2_commit",
255 )
256 .unwrap();
257
258 assert_eq!(
261 push_branch(
262 &tmp_other_repo_dir.path().to_str().unwrap().into(),
263 "origin",
264 "master",
265 false,
266 false,
267 None,
268 None,
269 )
270 .is_err(),
271 true
272 );
273
274 assert_eq!(
277 push_branch(
278 &tmp_other_repo_dir.path().to_str().unwrap().into(),
279 "origin",
280 "master",
281 true,
282 false,
283 None,
284 None,
285 )
286 .is_err(),
287 false
288 );
289 }
290
291 #[test]
292 fn test_force_push_rewrites_history() {
293 let (tmp_repo_dir, repo) = repo_init().unwrap();
298 let (tmp_other_repo_dir, other_repo) = repo_init().unwrap();
299 let (tmp_upstream_dir, upstream) = repo_init_bare().unwrap();
300
301 repo.remote(
302 "origin",
303 tmp_upstream_dir.path().to_str().unwrap(),
304 )
305 .unwrap();
306
307 other_repo
308 .remote(
309 "origin",
310 tmp_upstream_dir.path().to_str().unwrap(),
311 )
312 .unwrap();
313
314 let tmp_repo_file_path =
315 tmp_repo_dir.path().join("temp_file.txt");
316 let mut tmp_repo_file =
317 File::create(tmp_repo_file_path).unwrap();
318 writeln!(tmp_repo_file, "TempSomething").unwrap();
319
320 sync::stage_add_file(
321 &tmp_repo_dir.path().to_str().unwrap().into(),
322 Path::new("temp_file.txt"),
323 )
324 .unwrap();
325
326 let repo_1_commit = sync::commit(
327 &tmp_repo_dir.path().to_str().unwrap().into(),
328 "repo_1_commit",
329 )
330 .unwrap();
331
332 assert_eq!(
334 sync::get_commit_files(
335 &tmp_repo_dir.path().to_str().unwrap().into(),
336 repo_1_commit,
337 None
338 )
339 .unwrap()[0]
340 .path,
341 String::from("temp_file.txt")
342 );
343
344 let commits = get_commit_ids(&repo, 1);
345 assert!(commits.contains(&repo_1_commit));
346
347 push_branch(
348 &tmp_repo_dir.path().to_str().unwrap().into(),
349 "origin",
350 "master",
351 false,
352 false,
353 None,
354 None,
355 )
356 .unwrap();
357
358 let tmp_other_repo_file_path =
359 tmp_other_repo_dir.path().join("temp_file.txt");
360 let mut tmp_other_repo_file =
361 File::create(tmp_other_repo_file_path).unwrap();
362 writeln!(tmp_other_repo_file, "TempElse").unwrap();
363
364 sync::stage_add_file(
365 &tmp_other_repo_dir.path().to_str().unwrap().into(),
366 Path::new("temp_file.txt"),
367 )
368 .unwrap();
369
370 let repo_2_commit = sync::commit(
371 &tmp_other_repo_dir.path().to_str().unwrap().into(),
372 "repo_2_commit",
373 )
374 .unwrap();
375
376 let repo_2_parent = other_repo
377 .find_commit(repo_2_commit.into())
378 .unwrap()
379 .parents()
380 .next()
381 .unwrap()
382 .id();
383
384 let commits = get_commit_ids(&other_repo, 1);
385 assert!(commits.contains(&repo_2_commit));
386
387 assert_eq!(
390 push_branch(
391 &tmp_other_repo_dir.path().to_str().unwrap().into(),
392 "origin",
393 "master",
394 false,
395 false,
396 None,
397 None,
398 )
399 .is_err(),
400 true
401 );
402
403 let commits = get_commit_ids(&upstream, 1);
406 assert!(!commits.contains(&repo_2_commit));
407
408 push_branch(
412 &tmp_other_repo_dir.path().to_str().unwrap().into(),
413 "origin",
414 "master",
415 true,
416 false,
417 None,
418 None,
419 )
420 .unwrap();
421
422 let commits = get_commit_ids(&upstream, 1);
423 assert!(commits.contains(&repo_2_commit));
424
425 let new_upstream_parent =
426 Repository::init_bare(tmp_upstream_dir.path())
427 .unwrap()
428 .find_commit(repo_2_commit.into())
429 .unwrap()
430 .parents()
431 .next()
432 .unwrap()
433 .id();
434 assert_eq!(new_upstream_parent, repo_2_parent,);
435 }
436
437 #[test]
438 fn test_delete_remote_branch() {
439 let (upstream_dir, upstream_repo) = repo_init_bare().unwrap();
443
444 let (tmp_repo_dir, repo) =
445 repo_clone(upstream_dir.path().to_str().unwrap())
446 .unwrap();
447
448 let commit_1 = write_commit_file(
450 &repo,
451 "temp_file.txt",
452 "SomeContent",
453 "Initial commit",
454 );
455
456 let commits = get_commit_ids(&repo, 1);
457 assert!(commits.contains(&commit_1));
458
459 push_branch(
460 &tmp_repo_dir.path().to_str().unwrap().into(),
461 "origin",
462 "master",
463 false,
464 false,
465 None,
466 None,
467 )
468 .unwrap();
469
470 sync::create_branch(
472 &tmp_repo_dir.path().to_str().unwrap().into(),
473 "test_branch",
474 )
475 .unwrap();
476
477 push_branch(
479 &tmp_repo_dir.path().to_str().unwrap().into(),
480 "origin",
481 "test_branch",
482 false,
483 false,
484 None,
485 None,
486 )
487 .unwrap();
488
489 assert_eq!(
491 upstream_repo
492 .branches(None)
493 .unwrap()
494 .map(std::result::Result::unwrap)
495 .map(|(i, _)| i.name().unwrap().unwrap().to_string())
496 .any(|i| &i == "test_branch"),
497 true
498 );
499
500 assert_eq!(
502 push_branch(
503 &tmp_repo_dir.path().to_str().unwrap().into(),
504 "origin",
505 "test_branch",
506 false,
507 true,
508 None,
509 None,
510 )
511 .is_ok(),
512 true
513 );
514
515 assert_eq!(
517 upstream_repo
518 .branches(None)
519 .unwrap()
520 .map(std::result::Result::unwrap)
521 .map(|(i, _)| i.name().unwrap().unwrap().to_string())
522 .any(|i| &i == "test_branch"),
523 false
524 );
525 }
526}