Skip to main content

workon/
clone.rs

1use std::path::PathBuf;
2
3use git2::{build::RepoBuilder, FetchOptions, Repository};
4use log::debug;
5
6use crate::error::Result;
7use crate::{convert_to_bare, get_default_branch_name, get_remote_callbacks_default};
8
9/// Options for [`clone`].
10pub struct CloneOptions {
11    /// Called during the network transfer with `(received_objects, total_objects, received_bytes)`.
12    pub on_transfer_progress: Box<dyn FnMut(usize, usize, usize)>,
13}
14
15impl Default for CloneOptions {
16    fn default() -> Self {
17        Self {
18            on_transfer_progress: Box::new(|_, _, _| {}),
19        }
20    }
21}
22
23/// Clone a remote repository into the worktrees layout.
24///
25/// The repository is cloned as a bare repo at `<path>/.bare` and a `.git` link
26/// file is written at `<path>/.git` so that standard git tooling continues to
27/// work. The fetch refspec for `origin` is configured to fetch all branches.
28///
29/// If `path` already ends with `.bare` it is used as-is; otherwise `.bare` is
30/// appended.
31pub fn clone(path: PathBuf, url: &str, options: CloneOptions) -> Result<Repository> {
32    let CloneOptions {
33        mut on_transfer_progress,
34    } = options;
35    debug!("path {}", path.display());
36    let path = if path.ends_with(".bare") {
37        debug!("ended with .bare!");
38        path
39    } else {
40        debug!("didn't end with .bare!");
41        path.join(".bare")
42    };
43
44    debug!("final path {}", path.display());
45
46    let auth = get_remote_callbacks_default(Some(url))?;
47    let mut callbacks = auth.callbacks();
48    callbacks.transfer_progress(move |progress| {
49        on_transfer_progress(
50            progress.received_objects(),
51            progress.total_objects(),
52            progress.received_bytes(),
53        );
54        true
55    });
56
57    let mut fetch_options = FetchOptions::new();
58    fetch_options.remote_callbacks(callbacks);
59
60    let mut builder = RepoBuilder::new();
61    builder.bare(true);
62    builder.fetch_options(fetch_options);
63    builder.remote_create(|repo, name, url| {
64        debug!("Creating remote {} at {}", name, url);
65        let remote = repo.remote(name, url)?;
66
67        match get_default_branch_name(repo, Some(remote)) {
68            Ok(default_branch) => {
69                debug!("Default branch: {}", default_branch);
70                repo.remote_add_fetch(
71                    name,
72                    format!(
73                        "+refs/heads/{default_branch}:refs/remotes/origin/{default_branch}",
74                        default_branch = default_branch
75                    )
76                    .as_str(),
77                )?;
78                repo.find_remote(name)
79            }
80            Err(_) => {
81                debug!("No default branch found");
82                repo.remote(name, url)
83            }
84        }
85    });
86
87    debug!("Cloning {} into {}", url, path.display());
88
89    // 1. git clone --single-branch <url>.git <path>/.bare
90    let repo = builder.clone(url, &path)?;
91    // 2. $ echo "gitdir: ./.bare" > .git
92    // 3. $ git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
93    convert_to_bare(repo)
94}