gnostr_asyncgit/sync/remotes/
push.rs

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
17///
18pub trait AsyncProgress: Clone + Send + Sync {
19    ///
20    fn is_done(&self) -> bool;
21    ///
22    fn progress(&self) -> ProgressPercent;
23}
24
25///
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum ProgressNotification {
28    ///
29    UpdateTips {
30        ///
31        name: String,
32        ///
33        a: CommitId,
34        ///
35        b: CommitId,
36    },
37    ///
38    Transfer {
39        ///
40        objects: usize,
41        ///
42        total_objects: usize,
43    },
44    ///
45    PushTransfer {
46        ///
47        current: usize,
48        ///
49        total: usize,
50        ///
51        bytes: usize,
52    },
53    ///
54    Packing {
55        ///
56        stage: PackBuilderStage,
57        ///
58        total: usize,
59        ///
60        current: usize,
61    },
62    ///
63    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///
93#[derive(Copy, Clone, Debug)]
94pub enum PushType {
95    ///
96    Branch,
97    ///
98    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//TODO: clenaup
130#[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        // This test mimics the scenario of 2 people having 2
195        // local branches and both modifying the same file then
196        // both pushing, sequentially
197        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        // Attempt a normal push,
240        // should fail as branches diverged
241        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        // Attempt force push,
256        // should work as it forces the push through
257        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        // This test mimics the scenario of 2 people having 2
275        // local branches and both modifying the same file then
276        // both pushing, sequentially
277
278        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        //NOTE: make sure the commit actually contains that file
306        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        // Attempt a normal push,
359        // should fail as branches diverged
360        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        // Check that the other commit is not in upstream,
375        // a normal push would not rewrite history
376        let commits = get_commit_ids(&upstream, 1);
377        assert!(!commits.contains(&repo_2_commit));
378
379        // Attempt force push,
380        // should work as it forces the push through
381
382        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        // This test mimics the scenario of a user creating a branch,
410        // push it, and then remove it on the remote
411
412        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        // You need a commit before being able to branch !
417        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        // Create the local branch
434        sync::create_branch(&tmp_repo_dir.path().to_str().unwrap().into(), "test_branch").unwrap();
435
436        // Push the local branch
437        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        // Test if the branch exits on the remote
449        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        // Delete the remote branch
460        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        // Test that the branch has be remove from the remote
475        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}