1use crate::models::{CheckoutBinding, ProjectContext, RepositoryFingerprint};
2use crate::project_metadata;
3use std::path::{Path, PathBuf};
4use std::process::Command;
5
6pub fn discover_project_context(current_directory: &Path) -> ProjectContext {
7 let local_root = git_output(current_directory, ["rev-parse", "--show-toplevel"])
8 .map(PathBuf::from)
9 .unwrap_or_else(|| current_directory.to_path_buf());
10 let remote_url = git_output(&local_root, ["config", "--get", "remote.origin.url"]);
11 let branch = git_output(&local_root, ["rev-parse", "--abbrev-ref", "HEAD"]);
12 let last_commit = git_output(&local_root, ["rev-parse", "HEAD"]);
13 let default_branch = git_output(&local_root, ["symbolic-ref", "refs/remotes/origin/HEAD"])
14 .and_then(|value| value.rsplit('/').next().map(ToString::to_string));
15 let parsed_remote = remote_url.as_deref().and_then(parse_remote_url);
16 let project_slug = parsed_remote
17 .as_ref()
18 .map(|(_, _, repo_name)| repo_name.clone())
19 .or_else(|| local_root.file_name().map(|value| value.to_string_lossy().to_string()))
20 .unwrap_or_else(|| "project".to_string());
21
22 let fingerprint = RepositoryFingerprint {
23 remote_url,
24 host: parsed_remote.as_ref().map(|(host, _, _)| host.clone()),
25 owner: parsed_remote.as_ref().map(|(_, owner, _)| owner.clone()),
26 repo_name: parsed_remote.as_ref().map(|(_, _, repo_name)| repo_name.clone()),
27 default_branch,
28 };
29
30 let checkout_binding = CheckoutBinding {
31 project_id: "pending".to_string(),
32 local_root: Some(local_root.to_string_lossy().to_string()),
33 branch,
34 last_commit,
35 client_label: detect_client_label(),
36 last_sync: None,
37 };
38
39 ProjectContext {
40 project_slug,
41 project_root: local_root.to_string_lossy().to_string(),
42 fingerprint,
43 checkout_binding,
44 project_metadata: project_metadata::build_project_metadata(&local_root).ok(),
45 }
46}
47
48pub fn discover_repository_context(current_directory: &Path) -> ProjectContext {
49 discover_project_context(current_directory)
50}
51
52fn parse_remote_url(remote_url: &str) -> Option<(String, String, String)> {
53 let trimmed = remote_url.trim().trim_end_matches('/').trim_end_matches(".git");
54 if let Some(rest) = trimmed.strip_prefix("https://") {
55 return parse_host_path(rest);
56 }
57
58 if let Some(rest) = trimmed.strip_prefix("http://") {
59 return parse_host_path(rest);
60 }
61
62 if let Some(rest) = trimmed.strip_prefix("ssh://git@") {
63 return parse_host_path(rest);
64 }
65
66 if let Some(rest) = trimmed.strip_prefix("git@") {
67 let (host, path) = rest.split_once(':')?;
68 return parse_path_segments(host, path);
69 }
70
71 None
72}
73
74fn parse_host_path(value: &str) -> Option<(String, String, String)> {
75 let (host, path) = value.split_once('/')?;
76 parse_path_segments(host, path)
77}
78
79fn parse_path_segments(host: &str, path: &str) -> Option<(String, String, String)> {
80 let mut segments = path.split('/').filter(|segment| !segment.is_empty());
81 let owner = segments.next()?.to_string();
82 let repo_name = segments.next()?.to_string();
83 Some((host.to_string(), owner, repo_name))
84}
85
86fn git_output<const N: usize>(working_directory: &Path, args: [&str; N]) -> Option<String> {
87 let output = Command::new("git")
88 .args(args)
89 .current_dir(working_directory)
90 .output()
91 .ok()?;
92 if !output.status.success() {
93 return None;
94 }
95
96 let value = String::from_utf8(output.stdout).ok()?;
97 let trimmed = value.trim();
98 if trimmed.is_empty() {
99 return None;
100 }
101
102 Some(trimmed.to_string())
103}
104
105fn detect_client_label() -> Option<String> {
106 std::env::var("COMPUTERNAME")
107 .ok()
108 .filter(|value| !value.trim().is_empty())
109 .or_else(|| std::env::var("HOSTNAME").ok().filter(|value| !value.trim().is_empty()))
110}
111
112#[cfg(test)]
113mod tests {
114 use super::parse_remote_url;
115
116 #[test]
117 fn parse_remote_url_supports_https_and_ssh_formats() {
118 assert_eq!(
119 parse_remote_url("https://github.com/MarkBovee/nebu-ctx.git"),
120 Some(("github.com".to_string(), "MarkBovee".to_string(), "nebu-ctx".to_string()))
121 );
122
123 assert_eq!(
124 parse_remote_url("git@github.com:MarkBovee/nebu-ctx.git"),
125 Some(("github.com".to_string(), "MarkBovee".to_string(), "nebu-ctx".to_string()))
126 );
127 }
128}