1+ //! A Rust program that scans directories for Git repositories, checks branches,
2+ //! and displays their ahead/behind status relative to the main branch.
3+ //!
4+ //! This program uses the `walkdir` crate for directory traversal, the `regex` crate
5+ //! for parsing branch data, and executes Git commands to gather repository information.
6+
17use std:: env;
2- use std:: path:: Path ;
38use std:: process:: { Command , Stdio } ;
49use std:: str;
510use walkdir:: WalkDir ;
611use regex:: Regex ;
712
13+ /// ANSI escape codes for colored terminal output
814const RED : & str = "\x1b [0;31m" ;
915const GREEN : & str = "\x1b [0;32m" ;
1016const NO_COLOR : & str = "\x1b [0m" ;
1117const BLUE : & str = "\x1b [0;34m" ;
1218const YELLOW : & str = "\x1b [0;33m" ;
1319
14- fn count_commits ( dir : & str , branch : & str , base_branch : & str ) -> ( i32 , i32 ) {
20+ /// Runs a Git command with the provided arguments and returns its trimmed output.
21+ ///
22+ /// # Arguments
23+ ///
24+ /// * `args` - A slice of string slices containing the Git command arguments.
25+ ///
26+ /// # Returns
27+ ///
28+ /// A `String` containing the trimmed output of the Git command.
29+ fn run_command_and_trim ( args : & [ & str ] ) -> String {
1530 let output = Command :: new ( "git" )
16- . args ( & [ "-C" , dir , "rev-list" , "--left-right" , "--count" , & format ! ( "{}...{}" , base_branch , branch ) ] )
31+ . args ( args )
1732 . output ( )
18- . expect ( "Failed to execute git rev-list command" ) ;
33+ . expect ( "Failed to execute git command" ) ;
34+
35+ str:: from_utf8 ( & output. stdout )
36+ . expect ( "Command output is not valid UTF-8" )
37+ . trim ( )
38+ . to_string ( )
39+ }
1940
20- let ahead_behind = str:: from_utf8 ( & output. stdout ) . unwrap ( ) . trim ( ) ;
21- let parts: Vec < & str > = ahead_behind. split ( '\t' ) . collect ( ) ;
41+ /// Counts the commits that are ahead and behind between two branches.
42+ ///
43+ /// # Arguments
44+ ///
45+ /// * `dir` - The path to the Git repository.
46+ /// * `branch` - The branch being compared.
47+ /// * `base_branch` - The base branch for comparison.
48+ ///
49+ /// # Returns
50+ ///
51+ /// A tuple `(ahead, behind)` where `ahead` is the number of commits the branch is ahead
52+ /// of the base branch, and `behind` is the number of commits it is behind.
53+ fn count_commits ( dir : & str , branch : & str , base_branch : & str ) -> ( i32 , i32 ) {
54+ let output = run_command_and_trim ( & [
55+ "-C" ,
56+ dir,
57+ "rev-list" ,
58+ "--left-right" ,
59+ "--count" ,
60+ & format ! ( "{}...{}" , base_branch, branch) ,
61+ ] ) ;
62+
63+ let parts: Vec < & str > = output. split ( '\t' ) . collect ( ) ;
2264 let behind = parts[ 0 ] . parse ( ) . unwrap_or ( 0 ) ;
2365 let ahead = parts[ 1 ] . parse ( ) . unwrap_or ( 0 ) ;
2466 ( ahead, behind)
2567}
2668
69+ /// Checks if a given directory is a Git repository.
70+ ///
71+ /// # Arguments
72+ ///
73+ /// * `path` - The path to the directory being checked.
74+ ///
75+ /// # Returns
76+ ///
77+ /// `true` if the directory is a Git repository, `false` otherwise.
2778fn is_git_repo ( path : & str ) -> bool {
28- Command :: new ( "git" )
29- . args ( & [ "-C" , path, "rev-parse" ] )
79+ let status = Command :: new ( "git" )
80+ . args ( & [ "-C" , path, "rev-parse" , "--is-inside-work-tree" ] )
3081 . stderr ( Stdio :: null ( ) )
31- . status ( )
32- . map_or ( false , |status| status. success ( ) )
82+ . output ( ) ;
83+
84+ match status {
85+ Ok ( output) => output. status . success ( ) ,
86+ Err ( _) => {
87+ eprintln ! ( "Failed to check if '{}' is a git repository" , path) ;
88+ false
89+ }
90+ }
3391}
3492
93+ /// Processes a Git repository and displays information about its branches.
94+ ///
95+ /// # Arguments
96+ ///
97+ /// * `dir` - The path to the Git repository.
3598fn process_repo ( dir : & str ) {
36- let repo_name = Command :: new ( "git" )
37- . args ( & [ "-C" , dir, "remote" , "get-url" , "origin" ] )
38- . output ( )
39- . expect ( "Failed to get remote URL" )
40- . stdout ;
41-
42- let repo_name = str:: from_utf8 ( & repo_name)
43- . unwrap ( )
44- . trim ( )
99+ // Get the repository name by retrieving the remote URL.
100+ let repo_name = run_command_and_trim ( & [ "-C" , dir, "remote" , "get-url" , "origin" ] )
45101 . replace ( ".git" , "" ) ;
46102
47103 println ! ( "Repo: {}" , repo_name) ;
48104
49- let main_branch = Command :: new ( "git" )
50- . args ( & [ "-C" , dir, "rev-parse" , "HEAD" ] )
51- . output ( )
52- . expect ( "Failed to get main branch" )
53- . stdout ;
54-
55- let main_branch = str:: from_utf8 ( & main_branch) . unwrap ( ) . trim ( ) ;
105+ // Get the main branch (HEAD).
106+ let main_branch = run_command_and_trim ( & [ "-C" , dir, "rev-parse" , "HEAD" ] ) ;
56107
108+ // Print the header for branch information.
57109 println ! (
58110 "{}{:5} {}{:6} {}{:30} {}{:20} {}{:40}" ,
59111 GREEN , "Ahead" , RED , "Behind" , BLUE , "Branch" , YELLOW , "Last Commit" , NO_COLOR , " "
@@ -63,15 +115,19 @@ fn process_repo(dir: &str) {
63115 GREEN , "-----" , RED , "------" , BLUE , "------------------------------" , YELLOW , "-------------------" , NO_COLOR , " "
64116 ) ;
65117
66- let branches_output = Command :: new ( "git" )
67- . args ( & [ "-C" , dir, "for-each-ref" , "--sort=-authordate" , "--format=%(objectname:short)@%(refname:short)@%(committerdate:relative)" , "refs/heads/" ] )
68- . output ( )
69- . expect ( "Failed to list branches" )
70- . stdout ;
71-
72- let branch_output_str = str:: from_utf8 ( & branches_output) . expect ( "Invalid UTF-8 in branch output" ) ;
118+ // Retrieve branch information using `git for-each-ref`.
119+ let branches_output = run_command_and_trim ( & [
120+ "-C" ,
121+ dir,
122+ "for-each-ref" ,
123+ "--sort=-authordate" ,
124+ "--format=%(objectname:short)@%(refname:short)@%(committerdate:relative)" ,
125+ "refs/heads/" ,
126+ ] ) ;
127+
128+ // Regex to parse branch data.
73129 let branch_regex = Regex :: new ( r"([^\@]+)@([^\@]+)@([^\@]+)" ) . unwrap ( ) ;
74- let branches = branch_output_str . trim ( ) . lines ( ) ;
130+ let branches = branches_output . trim ( ) . lines ( ) ;
75131
76132 for branchdata in branches {
77133 if let Some ( caps) = branch_regex. captures ( branchdata) {
@@ -80,7 +136,7 @@ fn process_repo(dir: &str) {
80136 let time = & caps[ 3 ] ;
81137
82138 if branch != main_branch {
83- let ( ahead, behind) = count_commits ( dir, sha, main_branch) ;
139+ let ( ahead, behind) = count_commits ( dir, sha, & main_branch) ;
84140 println ! (
85141 "{}{:5} {}{:6} {}{:30} {}{:20} {}{:40}" ,
86142 GREEN , ahead, RED , behind, BLUE , branch, YELLOW , time, NO_COLOR , ""
@@ -91,26 +147,35 @@ fn process_repo(dir: &str) {
91147 println ! ( ) ;
92148}
93149
94- fn check_all_dirs ( path : & Path , depth : usize ) {
95- for entry in WalkDir :: new ( path)
150+ /// The entry point of the program. Scans directories for Git repositories and processes them.
151+ fn main ( ) {
152+ // Parse command-line arguments for depth.
153+ let args: Vec < String > = env:: args ( ) . collect ( ) ;
154+ let depth = args. get ( 1 ) . and_then ( |d| d. parse ( ) . ok ( ) ) . unwrap_or ( 0 ) ;
155+
156+ // Get the current working directory.
157+ let current_dir = env:: current_dir ( ) . expect ( "Failed to get current directory" ) ;
158+
159+ println ! ( "Scanning '{}' up to a depth of {}" , current_dir. display( ) , depth) ;
160+
161+ let mut found_repos = false ;
162+
163+ // Walk directories using `walkdir`.
164+ for entry in WalkDir :: new ( & current_dir)
96165 . min_depth ( 0 )
97166 . max_depth ( depth)
98167 . into_iter ( )
99168 . filter_map ( Result :: ok)
100169 {
101170 let path = entry. path ( ) ;
102- if path. is_dir ( ) {
103- if is_git_repo ( path. to_str ( ) . unwrap ( ) ) {
104- process_repo ( path. to_str ( ) . unwrap ( ) ) ;
105- }
171+ if path. is_dir ( ) && is_git_repo ( path. to_str ( ) . unwrap ( ) ) {
172+ found_repos = true ;
173+ process_repo ( path. to_str ( ) . unwrap ( ) ) ;
106174 }
107175 }
108- }
109-
110- fn main ( ) {
111- let args: Vec < String > = env:: args ( ) . collect ( ) ;
112- let depth = args. get ( 1 ) . and_then ( |d| d. parse ( ) . ok ( ) ) . unwrap_or ( 0 ) ;
113- let current_dir = env:: current_dir ( ) . expect ( "Failed to get current directory" ) ;
114176
115- check_all_dirs ( & current_dir, depth) ;
177+ // If no repositories are found, notify the user.
178+ if !found_repos {
179+ println ! ( "No git repositories found in '{}'." , current_dir. display( ) ) ;
180+ }
116181}
0 commit comments