summaryrefslogtreecommitdiffstats
path: root/vendor/gix/src/remote/save.rs
blob: 2a91dfa9c1de9ae735b2a66caa3351c965f4e6bf (plain)
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
use std::convert::TryInto;

use gix_macros::momo;

use crate::{
    bstr::{BStr, BString},
    config, remote, Remote,
};

/// The error returned by [`Remote::save_to()`].
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
    #[error("The remote pointing to {} is anonymous and can't be saved.", url.to_bstring())]
    NameMissing { url: gix_url::Url },
}

/// The error returned by [`Remote::save_as_to()`].
///
/// Note that this type should rather be in the `as` module, but cannot be as it's part of the Rust syntax.
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum AsError {
    #[error(transparent)]
    Save(#[from] Error),
    #[error(transparent)]
    Name(#[from] crate::remote::name::Error),
}

/// Serialize into git-config.
impl Remote<'_> {
    /// Save ourselves to the given `config` if we are a named remote or fail otherwise.
    ///
    /// Note that all sections named `remote "<name>"` will be cleared of all values we are about to write,
    /// and the last `remote "<name>"` section will be containing all relevant values so that reloading the remote
    /// from `config` would yield the same in-memory state.
    #[allow(clippy::result_large_err)]
    pub fn save_to(&self, config: &mut gix_config::File<'static>) -> Result<(), Error> {
        fn as_key(name: &str) -> gix_config::parse::section::Key<'_> {
            name.try_into().expect("valid")
        }
        let name = self.name().ok_or_else(|| Error::NameMissing {
            url: self
                .url
                .as_ref()
                .or(self.push_url.as_ref())
                .expect("one url is always set")
                .to_owned(),
        })?;
        if let Some(section_ids) = config.sections_and_ids_by_name("remote").map(|it| {
            it.filter_map(|(s, id)| (s.header().subsection_name() == Some(name.as_bstr())).then_some(id))
                .collect::<Vec<_>>()
        }) {
            let mut sections_to_remove = Vec::new();
            const KEYS_TO_REMOVE: &[&str] = &[
                config::tree::Remote::URL.name,
                config::tree::Remote::PUSH_URL.name,
                config::tree::Remote::FETCH.name,
                config::tree::Remote::PUSH.name,
                config::tree::Remote::TAG_OPT.name,
            ];
            for id in section_ids {
                let mut section = config.section_mut_by_id(id).expect("just queried");
                let was_empty = section.num_values() == 0;

                for key in KEYS_TO_REMOVE {
                    while section.remove(key).is_some() {}
                }

                let is_empty_after_deletions_of_values_to_be_written = section.num_values() == 0;
                if !was_empty && is_empty_after_deletions_of_values_to_be_written {
                    sections_to_remove.push(id);
                }
            }
            for id in sections_to_remove {
                config.remove_section_by_id(id);
            }
        }
        let mut section = config
            .section_mut_or_create_new("remote", Some(name.as_ref()))
            .expect("section name is validated and 'remote' is acceptable");
        if let Some(url) = self.url.as_ref() {
            section.push(as_key("url"), Some(url.to_bstring().as_ref()));
        }
        if let Some(url) = self.push_url.as_ref() {
            section.push(as_key("pushurl"), Some(url.to_bstring().as_ref()));
        }
        if self.fetch_tags != Default::default() {
            section.push(
                as_key(config::tree::Remote::TAG_OPT.name),
                BStr::new(match self.fetch_tags {
                    remote::fetch::Tags::All => "--tags",
                    remote::fetch::Tags::None => "--no-tags",
                    remote::fetch::Tags::Included => unreachable!("BUG: the default shouldn't be written and we try"),
                })
                .into(),
            );
        }
        for (key, spec) in self
            .fetch_specs
            .iter()
            .map(|spec| ("fetch", spec))
            .chain(self.push_specs.iter().map(|spec| ("push", spec)))
        {
            section.push(as_key(key), Some(spec.to_ref().to_bstring().as_ref()));
        }
        Ok(())
    }

    /// Forcefully set our name to `name` and write our state to `config` similar to [`save_to()`][Self::save_to()].
    ///
    /// Note that this sets a name for anonymous remotes, but overwrites the name for those who were named before.
    /// If this name is different from the current one, the git configuration will still contain the previous name,
    /// and the caller should account for that.
    #[allow(clippy::result_large_err)]
    #[momo]
    pub fn save_as_to(
        &mut self,
        name: impl Into<BString>,
        config: &mut gix_config::File<'static>,
    ) -> Result<(), AsError> {
        let name = crate::remote::name::validated(name)?;
        let prev_name = self.name.take();
        self.name = Some(name.into());
        self.save_to(config).map_err(|err| {
            self.name = prev_name;
            err.into()
        })
    }
}