git_next_core/git/
repo_details.rs

1//
2use crate::{
3    git::{
4        self,
5        repository::open::{oreal::RealOpenRepository, OpenRepositoryLike},
6        Generation,
7    },
8    pike, s, BranchName, ForgeAlias, ForgeConfig, ForgeDetails, GitDir, Hostname, RemoteUrl,
9    RepoAlias, RepoConfig, RepoPath, ServerRepoConfig, StoragePathType,
10};
11
12use std::sync::{Arc, RwLock};
13
14use secrecy::{ExposeSecret, SecretString};
15use tracing::instrument;
16
17/// The derived information about a repo, used to interact with it
18#[derive(Clone, Debug, derive_more::Display, derive_with::With)]
19#[display("gen-{}:{}:{}/{}", generation, forge.forge_type(), forge.forge_alias(), repo_alias )]
20pub struct RepoDetails {
21    pub generation: Generation,
22    pub repo_alias: RepoAlias,
23    pub repo_path: RepoPath,
24    pub branch: BranchName,
25    pub forge: ForgeDetails,
26    pub repo_config: Option<RepoConfig>,
27    pub gitdir: GitDir,
28}
29impl RepoDetails {
30    #[must_use]
31    pub fn new(
32        generation: Generation,
33        repo_alias: &RepoAlias,
34        server_repo_config: &ServerRepoConfig,
35        forge_alias: &ForgeAlias,
36        forge_config: &ForgeConfig,
37        gitdir: GitDir,
38    ) -> Self {
39        Self {
40            generation,
41            repo_alias: repo_alias.clone(),
42            repo_path: server_repo_config.repo(),
43            repo_config: server_repo_config.repo_config(),
44            branch: server_repo_config.branch(),
45            gitdir,
46            forge: ForgeDetails::new(
47                forge_alias.clone(),
48                forge_config.forge_type(),
49                forge_config.hostname(),
50                forge_config.user(),
51                forge_config.token(),
52                forge_config.max_dev_commits(),
53            ),
54        }
55    }
56    pub(crate) fn origin(&self) -> secrecy::SecretString {
57        let repo_details = self;
58        let user = &repo_details.forge.user();
59        let hostname = &repo_details.forge.hostname();
60
61        let repo_path = &repo_details.repo_path;
62        let expose_secret = repo_details.forge.token();
63
64        let token = expose_secret.expose_secret();
65        let origin = format!("https://{user}:{token}@{hostname}/{repo_path}.git");
66        origin.into()
67    }
68
69    pub(crate) const fn gitdir(&self) -> &GitDir {
70        &self.gitdir
71    }
72
73    #[must_use]
74    pub fn with_hostname(mut self, hostname: Hostname) -> Self {
75        let forge = self.forge;
76        self.forge = forge.with_hostname(hostname);
77        self
78    }
79
80    // url is a secret as it contains auth token
81    pub(crate) fn url(&self) -> SecretString {
82        let user = self.forge.user();
83        let token = self.forge.token().expose_secret();
84        let auth_delim = if token.is_empty() { "" } else { ":" };
85        let hostname = self.forge.hostname();
86        let repo_path = &self.repo_path;
87        format!("https://{user}{auth_delim}{token}@{hostname}/{repo_path}.git").into()
88    }
89
90    #[allow(clippy::result_large_err)]
91    pub(crate) fn open(&self) -> Result<impl OpenRepositoryLike, git::validation::remotes::Error> {
92        let gix_repo = pike! {
93            self
94            |> Self::gitdir
95            |> GitDir::pathbuf
96            |> gix::ThreadSafeRepository::open
97        }?;
98        let repo = RealOpenRepository::new(Arc::new(RwLock::new(gix_repo)), self.forge.clone());
99        Ok(repo)
100    }
101
102    #[must_use]
103    pub fn remote_url(&self) -> Option<RemoteUrl> {
104        use secrecy::ExposeSecret;
105        RemoteUrl::parse(self.url().expose_secret())
106    }
107
108    #[instrument]
109    pub fn assert_remote_url(&self, found: Option<RemoteUrl>) -> git::repository::Result<()> {
110        let Some(found) = found else {
111            tracing::debug!("No remote url found to assert");
112            return Ok(());
113        };
114        let Some(expected) = self.remote_url() else {
115            tracing::debug!("No remote url to assert against");
116            return Ok(());
117        };
118        if !found.matches(&expected) {
119            tracing::debug!(?found, ?expected, "urls differ");
120            match self.gitdir.storage_path_type() {
121                StoragePathType::External => {
122                    tracing::debug!("Refusing to update an external repo - user must resolve this");
123                    return Err(git::repository::Error::MismatchDefaultFetchRemote {
124                        found: Box::new(found),
125                        expected: Box::new(expected),
126                    });
127                }
128                StoragePathType::Internal => {
129                    tracing::debug!(?expected, "Need to update config with new url");
130                    self.write_remote_url(&expected)?;
131                }
132            }
133        }
134        Ok(())
135    }
136
137    #[tracing::instrument]
138    pub fn write_remote_url(&self, url: &RemoteUrl) -> Result<(), kxio::fs::Error> {
139        if self.gitdir.storage_path_type() != StoragePathType::Internal {
140            return Ok(());
141        }
142        let fs = self.gitdir.as_fs();
143        // load config file
144        let config_filename = &self.gitdir.join("config");
145        let file = fs.file(config_filename);
146        let config_file = file.reader()?;
147        let mut config_lines = config_file
148            .lines()?
149            .map(ToOwned::to_owned)
150            .collect::<Vec<_>>();
151        tracing::debug!(?config_lines, "original file");
152        let url_line = format!(r#"   url = "{url}""#);
153        if config_lines
154            .iter()
155            .any(|line| line == r#"[remote "origin"]"#)
156        {
157            tracing::debug!("has an 'origin' remote");
158            config_lines
159                .iter_mut()
160                .filter(|line| line.starts_with(r"   url = "))
161                .for_each(|line| line.clone_from(&url_line));
162        } else {
163            tracing::debug!("has no 'origin' remote");
164            config_lines.push(s!(r#"[remote "origin"]"#));
165            config_lines.push(url_line);
166        }
167        tracing::debug!(?config_lines, "updated file");
168        // write config file back out
169        file.write(config_lines.join("\n"))?;
170        Ok(())
171    }
172}