1+ use std:: path:: Path ;
2+ use std:: process:: Command ;
3+ use serde:: { Deserialize , Serialize } ;
4+ use crate :: Result ;
5+
6+ #[ derive( Debug , Clone , Serialize , Deserialize , PartialEq ) ]
7+ pub struct GitRepositoryInfo {
8+ pub user_name : String ,
9+ pub repository_name : String ,
10+ pub remote_url : String ,
11+ pub branch : Option < String > ,
12+ pub commit_hash : Option < String > ,
13+ pub is_dirty : bool ,
14+ }
15+
16+ pub struct GitInfoExtractor ;
17+
18+ impl GitInfoExtractor {
19+ pub fn new ( ) -> Self {
20+ Self
21+ }
22+
23+ pub fn extract_git_info ( repo_path : & Path ) -> Result < Option < GitRepositoryInfo > > {
24+ // Canonicalize the path to handle relative paths like ../../
25+ let canonical_path = match repo_path. canonicalize ( ) {
26+ Ok ( path) => path,
27+ Err ( _) => {
28+ // If canonicalization fails, the path might not exist
29+ return Ok ( None ) ;
30+ }
31+ } ;
32+
33+ // Find git repository root (may be parent directory)
34+ let git_root = match Self :: find_git_repository_root ( & canonical_path) {
35+ Some ( root) => root,
36+ None => return Ok ( None ) ,
37+ } ;
38+
39+ let remote_url = Self :: get_remote_url ( & git_root) ?;
40+ if let Some ( ( user_name, repository_name) ) = Self :: parse_remote_url ( & remote_url) {
41+ let branch = Self :: get_current_branch ( & git_root) . ok ( ) ;
42+ let commit_hash = Self :: get_current_commit_hash ( & git_root) . ok ( ) ;
43+ let is_dirty = Self :: is_working_directory_dirty ( & git_root) . unwrap_or ( false ) ;
44+
45+ Ok ( Some ( GitRepositoryInfo {
46+ user_name,
47+ repository_name,
48+ remote_url,
49+ branch,
50+ commit_hash,
51+ is_dirty,
52+ } ) )
53+ } else {
54+ Ok ( None )
55+ }
56+ }
57+
58+ fn find_git_repository_root ( start_path : & Path ) -> Option < std:: path:: PathBuf > {
59+ let mut current_path = start_path;
60+
61+ loop {
62+ let git_dir = current_path. join ( ".git" ) ;
63+ if git_dir. exists ( ) {
64+ return Some ( current_path. to_path_buf ( ) ) ;
65+ }
66+
67+ // Move to parent directory
68+ match current_path. parent ( ) {
69+ Some ( parent) => current_path = parent,
70+ None => return None , // Reached root directory without finding .git
71+ }
72+ }
73+ }
74+
75+ #[ allow( dead_code) ]
76+ fn is_git_repository ( repo_path : & Path ) -> bool {
77+ let git_dir = repo_path. join ( ".git" ) ;
78+ git_dir. exists ( )
79+ }
80+
81+ fn get_remote_url ( repo_path : & Path ) -> Result < String > {
82+ let output = Command :: new ( "git" )
83+ . current_dir ( repo_path)
84+ . args ( [ "remote" , "get-url" , "origin" ] )
85+ . output ( )
86+ . map_err ( |e| crate :: GitTypeError :: IoError ( e) ) ?;
87+
88+ if !output. status . success ( ) {
89+ return Err ( crate :: GitTypeError :: ExtractionFailed ( "Failed to get remote URL" . to_string ( ) ) ) ;
90+ }
91+
92+ let url = String :: from_utf8_lossy ( & output. stdout ) . trim ( ) . to_string ( ) ;
93+ Ok ( url)
94+ }
95+
96+ fn parse_remote_url ( url : & str ) -> Option < ( String , String ) > {
97+ // Handle HTTPS URLs like https://github.com/user/repo.git
98+ if url. starts_with ( "https://github.com/" ) {
99+ let path = url. strip_prefix ( "https://github.com/" ) ?;
100+ let path = path. strip_suffix ( ".git" ) . unwrap_or ( path) ;
101+ let parts: Vec < & str > = path. split ( '/' ) . collect ( ) ;
102+ if parts. len ( ) == 2 {
103+ return Some ( ( parts[ 0 ] . to_string ( ) , parts[ 1 ] . to_string ( ) ) ) ;
104+ }
105+ }
106+
107+ // Handle SSH URLs like git@github.com:user/repo.git
108+ if url. starts_with ( "git@github.com:" ) {
109+ let path = url. strip_prefix ( "git@github.com:" ) ?;
110+ let path = path. strip_suffix ( ".git" ) . unwrap_or ( path) ;
111+ let parts: Vec < & str > = path. split ( '/' ) . collect ( ) ;
112+ if parts. len ( ) == 2 {
113+ return Some ( ( parts[ 0 ] . to_string ( ) , parts[ 1 ] . to_string ( ) ) ) ;
114+ }
115+ }
116+
117+ // Handle SSH URLs like ssh://git@github.com/user/repo.git or ssh://git@github.com/user/repo
118+ if url. starts_with ( "ssh://git@github.com/" ) {
119+ let path = url. strip_prefix ( "ssh://git@github.com/" ) ?;
120+ let path = path. strip_suffix ( ".git" ) . unwrap_or ( path) ;
121+ let parts: Vec < & str > = path. split ( '/' ) . collect ( ) ;
122+ if parts. len ( ) == 2 {
123+ return Some ( ( parts[ 0 ] . to_string ( ) , parts[ 1 ] . to_string ( ) ) ) ;
124+ }
125+ }
126+
127+ None
128+ }
129+
130+ fn get_current_branch ( repo_path : & Path ) -> Result < String > {
131+ let output = Command :: new ( "git" )
132+ . current_dir ( repo_path)
133+ . args ( [ "branch" , "--show-current" ] )
134+ . output ( )
135+ . map_err ( |e| crate :: GitTypeError :: IoError ( e) ) ?;
136+
137+ if !output. status . success ( ) {
138+ return Err ( crate :: GitTypeError :: ExtractionFailed ( "Failed to get current branch" . to_string ( ) ) ) ;
139+ }
140+
141+ let branch = String :: from_utf8_lossy ( & output. stdout ) . trim ( ) . to_string ( ) ;
142+ Ok ( branch)
143+ }
144+
145+ fn get_current_commit_hash ( repo_path : & Path ) -> Result < String > {
146+ let output = Command :: new ( "git" )
147+ . current_dir ( repo_path)
148+ . args ( [ "rev-parse" , "HEAD" ] )
149+ . output ( )
150+ . map_err ( |e| crate :: GitTypeError :: IoError ( e) ) ?;
151+
152+ if !output. status . success ( ) {
153+ return Err ( crate :: GitTypeError :: ExtractionFailed ( "Failed to get current commit hash" . to_string ( ) ) ) ;
154+ }
155+
156+ let hash = String :: from_utf8_lossy ( & output. stdout ) . trim ( ) . to_string ( ) ;
157+ Ok ( hash)
158+ }
159+
160+ fn is_working_directory_dirty ( repo_path : & Path ) -> Result < bool > {
161+ let output = Command :: new ( "git" )
162+ . current_dir ( repo_path)
163+ . args ( [ "status" , "--porcelain" ] )
164+ . output ( )
165+ . map_err ( |e| crate :: GitTypeError :: IoError ( e) ) ?;
166+
167+ if !output. status . success ( ) {
168+ return Err ( crate :: GitTypeError :: ExtractionFailed ( "Failed to check working directory status" . to_string ( ) ) ) ;
169+ }
170+
171+ let status = String :: from_utf8_lossy ( & output. stdout ) ;
172+ Ok ( !status. trim ( ) . is_empty ( ) )
173+ }
174+ }
0 commit comments