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};
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 mut callbacks = get_remote_callbacks()?;
47    callbacks.transfer_progress(move |progress| {
48        on_transfer_progress(
49            progress.received_objects(),
50            progress.total_objects(),
51            progress.received_bytes(),
52        );
53        true
54    });
55
56    let mut fetch_options = FetchOptions::new();
57    fetch_options.remote_callbacks(callbacks);
58
59    let mut builder = RepoBuilder::new();
60    builder.bare(true);
61    builder.fetch_options(fetch_options);
62    builder.remote_create(|repo, name, url| {
63        debug!("Creating remote {} at {}", name, url);
64        let remote = repo.remote(name, url)?;
65
66        match get_default_branch_name(repo, Some(remote)) {
67            Ok(default_branch) => {
68                debug!("Default branch: {}", default_branch);
69                repo.remote_add_fetch(
70                    name,
71                    format!(
72                        "+refs/heads/{default_branch}:refs/remotes/origin/{default_branch}",
73                        default_branch = default_branch
74                    )
75                    .as_str(),
76                )?;
77                repo.find_remote(name)
78            }
79            Err(_) => {
80                debug!("No default branch found");
81                repo.remote(name, url)
82            }
83        }
84    });
85
86    debug!("Cloning {} into {}", url, path.display());
87
88    // 1. git clone --single-branch <url>.git <path>/.bare
89    let repo = builder.clone(url, &path)?;
90    // 2. $ echo "gitdir: ./.bare" > .git
91    // 3. $ git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
92    convert_to_bare(repo)
93}