1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
use crate::OutputFormat;

pub struct Options {
    pub format: OutputFormat,
    pub bare: bool,
    pub handshake_info: bool,
    pub no_tags: bool,
    pub shallow: gix::remote::fetch::Shallow,
    pub ref_name: Option<gix::refs::PartialName>,
}

pub const PROGRESS_RANGE: std::ops::RangeInclusive<u8> = 1..=3;

pub(crate) mod function {
    use std::{borrow::Cow, ffi::OsStr};

    use anyhow::{bail, Context};
    use gix::{bstr::BString, remote::fetch::Status, NestedProgress};

    use super::Options;
    use crate::{repository::fetch::function::print_updates, OutputFormat};

    pub fn clone<P>(
        url: impl AsRef<OsStr>,
        directory: Option<impl Into<std::path::PathBuf>>,
        overrides: Vec<BString>,
        mut progress: P,
        mut out: impl std::io::Write,
        mut err: impl std::io::Write,
        Options {
            format,
            handshake_info,
            bare,
            no_tags,
            ref_name,
            shallow,
        }: Options,
    ) -> anyhow::Result<()>
    where
        P: NestedProgress,
        P::SubProgress: 'static,
    {
        if format != OutputFormat::Human {
            bail!("JSON output isn't yet supported for fetching.");
        }

        let url: gix::Url = url.as_ref().try_into()?;
        let directory = directory.map_or_else(
            || {
                let path = gix::path::from_bstr(Cow::Borrowed(url.path.as_ref()));
                if !bare && path.extension() == Some(OsStr::new("git")) {
                    path.file_stem().map(Into::into)
                } else {
                    path.file_name().map(Into::into)
                }
                .context("Filename extraction failed - path too short")
            },
            |dir| Ok(dir.into()),
        )?;
        let mut prepare = gix::clone::PrepareFetch::new(
            url,
            directory,
            if bare {
                gix::create::Kind::Bare
            } else {
                gix::create::Kind::WithWorktree
            },
            gix::create::Options::default(),
            {
                let mut opts = gix::open::Options::default().config_overrides(overrides);
                opts.permissions.config.git_binary = true;
                opts
            },
        )?;
        if no_tags {
            prepare = prepare.configure_remote(|r| Ok(r.with_fetch_tags(gix::remote::fetch::Tags::None)));
        }
        let (mut checkout, fetch_outcome) = prepare
            .with_shallow(shallow)
            .with_ref_name(ref_name.as_ref())?
            .fetch_then_checkout(&mut progress, &gix::interrupt::IS_INTERRUPTED)?;

        let (repo, outcome) = if bare {
            (checkout.persist(), None)
        } else {
            let (repo, outcome) = checkout.main_worktree(progress, &gix::interrupt::IS_INTERRUPTED)?;
            (repo, Some(outcome))
        };

        if handshake_info {
            writeln!(out, "Handshake Information")?;
            writeln!(out, "\t{:?}", fetch_outcome.ref_map.handshake)?;
        }

        match fetch_outcome.status {
            Status::NoPackReceived { dry_run, .. } => {
                assert!(!dry_run, "dry-run unsupported");
                writeln!(err, "The cloned repository appears to be empty")?;
            }
            Status::Change {
                update_refs, negotiate, ..
            } => {
                let remote = repo
                    .find_default_remote(gix::remote::Direction::Fetch)
                    .expect("one origin remote")?;
                let ref_specs = remote.refspecs(gix::remote::Direction::Fetch);
                print_updates(
                    &repo,
                    &negotiate,
                    update_refs,
                    ref_specs,
                    fetch_outcome.ref_map,
                    &mut out,
                    &mut err,
                )?;
            }
        };

        if let Some(gix::worktree::state::checkout::Outcome { collisions, errors, .. }) = outcome {
            if !(collisions.is_empty() && errors.is_empty()) {
                let mut messages = Vec::new();
                if !errors.is_empty() {
                    messages.push(format!("kept going through {} errors(s)", errors.len()));
                    for record in errors {
                        writeln!(err, "{}: {}", record.path, record.error).ok();
                    }
                }
                if !collisions.is_empty() {
                    messages.push(format!("encountered {} collision(s)", collisions.len()));
                    for col in collisions {
                        writeln!(err, "{}: collision ({:?})", col.path, col.error_kind).ok();
                    }
                }
                bail!(
                    "One or more errors occurred - checkout is incomplete: {}",
                    messages.join(", ")
                );
            }
        }
        Ok(())
    }
}