git_next_core/git/
repo_details.rs1use 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#[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 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 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 file.write(config_lines.join("\n"))?;
170 Ok(())
171 }
172}