@@ -4,6 +4,7 @@ use std::path::PathBuf;
44
55use crate :: command:: load_object;
66use crate :: internal:: branch:: Branch ;
7+ use crate :: internal:: config:: Config ;
78use crate :: internal:: head:: Head ;
89use clap:: Parser ;
910use colored:: Colorize ;
@@ -32,12 +33,85 @@ pub struct LogArgs {
3233 /// Show diffs for each commit (like git -p)
3334 #[ clap( short = 'p' , long = "patch" ) ]
3435 pub patch : bool ,
36+ /// Print out ref names of any commits that are shown
37+ #[ clap(
38+ long,
39+ default_missing_value = "short" ,
40+ require_equals = true ,
41+ num_args = 0 ..=1 ,
42+ ) ]
43+ pub decorate : Option < String > ,
44+ /// Do not print out ref names of any commits that are shown
45+ #[ clap( long) ]
46+ pub no_decorate : bool ,
3547
3648 /// Files to limit diff output (only used with -p)
3749 #[ clap( requires = "patch" , value_name = "PATHS" ) ]
3850 pathspec : Vec < String > ,
3951}
4052
53+ #[ derive( PartialEq ) ]
54+ enum DecorateOptions {
55+ No ,
56+ Short ,
57+ Full ,
58+ }
59+
60+ fn str_to_decorate_option ( s : & str ) -> Result < DecorateOptions , String > {
61+ match s {
62+ "no" => Ok ( DecorateOptions :: No ) ,
63+ "short" => Ok ( DecorateOptions :: Short ) ,
64+ "full" => Ok ( DecorateOptions :: Full ) ,
65+ "auto" => {
66+ if std:: io:: IsTerminal :: is_terminal ( & std:: io:: stdout ( ) ) {
67+ Ok ( DecorateOptions :: Short )
68+ } else {
69+ Ok ( DecorateOptions :: No )
70+ }
71+ }
72+ _ => Err ( s. to_owned ( ) ) ,
73+ }
74+ }
75+
76+ async fn determine_decorate_option ( args : & LogArgs ) -> Result < DecorateOptions , String > {
77+ let arg_deco = args
78+ . decorate
79+ . as_ref ( )
80+ . map ( |s| str_to_decorate_option ( s) )
81+ . transpose ( ) ?;
82+
83+ match arg_deco {
84+ Some ( a) => {
85+ if args. no_decorate {
86+ let args_os = std:: env:: args_os ( ) . peekable ( ) ;
87+ for arg in args_os {
88+ if arg == "--no-decorate" {
89+ return Ok ( a) ;
90+ } else if arg. to_str ( ) . unwrap_or_default ( ) . starts_with ( "--decorate" ) {
91+ return Ok ( DecorateOptions :: No ) ;
92+ } ;
93+ }
94+ } else {
95+ return Ok ( a) ;
96+ }
97+ }
98+ None => {
99+ if args. no_decorate {
100+ return Ok ( DecorateOptions :: No ) ;
101+ }
102+ }
103+ } ;
104+
105+ if let Some ( config_deco) = Config :: get ( "log" , None , "decorate" )
106+ . await
107+ . and_then ( |s| str_to_decorate_option ( & s) . ok ( ) )
108+ {
109+ Ok ( config_deco)
110+ } else {
111+ str_to_decorate_option ( "auto" )
112+ }
113+ }
114+
41115/// Get all reachable commits from the given commit hash
42116/// **didn't consider the order of the commits**
43117pub async fn get_reachable_commits ( commit_hash : String ) -> Vec < Commit > {
@@ -65,7 +139,25 @@ pub async fn get_reachable_commits(commit_hash: String) -> Vec<Commit> {
65139 reachable_commits
66140}
67141
142+ // Ordered as they should appear in log
143+ #[ derive( PartialEq , Eq , PartialOrd , Ord , Clone ) ]
144+ enum ReferenceKind {
145+ Tag , // decorate color = yellow
146+ Remote , // red
147+ Local , // green
148+ }
149+
150+ #[ derive( PartialEq , Eq , PartialOrd , Ord , Clone ) ]
151+ struct Reference {
152+ kind : ReferenceKind ,
153+ name : String ,
154+ }
155+
68156pub async fn execute ( args : LogArgs ) {
157+ let decorate_option = determine_decorate_option ( & args)
158+ . await
159+ . expect ( "fatal: invalid --decorate option" ) ;
160+
69161 #[ cfg( unix) ]
70162 let mut process = Command :: new ( "less" ) // create a pipe to less
71163 . arg ( "-R" ) // raw control characters
@@ -77,20 +169,25 @@ pub async fn execute(args: LogArgs) {
77169
78170 let head = Head :: current ( ) . await ;
79171 // check if the current branch has any commits
80- if let Head :: Branch ( branch_name) = head. to_owned ( ) {
81- let branch = Branch :: find_branch ( & branch_name, None ) . await ;
172+ let branch_name = if let Head :: Branch ( n) = head. to_owned ( ) {
173+ Some ( n)
174+ } else {
175+ None
176+ } ;
177+ if let Some ( n) = & branch_name {
178+ let branch = Branch :: find_branch ( n, None ) . await ;
82179 if branch. is_none ( ) {
83- panic ! ( "fatal: your current branch '{branch_name }' does not have any commits yet " ) ;
84- }
85- }
180+ panic ! ( "fatal: your current branch '{n }' does not have any commits yet " ) ;
181+ } ;
182+ } ;
86183
87184 let commit_hash = Head :: current_commit ( ) . await . unwrap ( ) . to_string ( ) ;
88185
89186 let mut reachable_commits = get_reachable_commits ( commit_hash. clone ( ) ) . await ;
90187 // default sort with signature time
91188 reachable_commits. sort_by ( |a, b| b. committer . timestamp . cmp ( & a. committer . timestamp ) ) ;
92189
93- let branch_commits = create_branch_commits_map ( ) . await ;
190+ let ref_commits = create_reference_commit_map ( ) . await ;
94191
95192 let max_output_number = min ( args. number . unwrap_or ( usize:: MAX ) , reachable_commits. len ( ) ) ;
96193 let mut output_number = 0 ;
@@ -100,66 +197,105 @@ pub async fn execute(args: LogArgs) {
100197 }
101198 output_number += 1 ;
102199
103- let branches = branch_commits. get ( & commit. id ) . cloned ( ) . unwrap_or_default ( ) ;
200+ let ref_msg = if decorate_option != DecorateOptions :: No {
201+ let mut ref_msgs: Vec < String > = vec ! [ ] ;
202+ if output_number == 1 {
203+ ref_msgs. push ( if let Some ( b_name) = & branch_name {
204+ format ! (
205+ "{} -> {}{}" ,
206+ "HEAD" . cyan( ) ,
207+ ( if decorate_option == DecorateOptions :: Full {
208+ "refs/heads/"
209+ } else {
210+ ""
211+ } )
212+ . green( ) ,
213+ b_name. green( )
214+ )
215+ } else {
216+ "HEAD" . cyan ( ) . to_string ( )
217+ } ) ;
218+ } ;
219+
220+ let mut refs = ref_commits. get ( & commit. id ) . cloned ( ) . unwrap_or_default ( ) ;
221+ refs. sort ( ) ;
222+
223+ ref_msgs. append (
224+ & mut refs
225+ . iter ( )
226+ . filter_map ( |r| {
227+ if r. kind == ReferenceKind :: Local && Some ( r. name . to_owned ( ) ) == branch_name
228+ {
229+ None
230+ } else {
231+ Some ( match r. kind {
232+ ReferenceKind :: Tag => format ! (
233+ "tag: {}{}" ,
234+ if decorate_option == DecorateOptions :: Full {
235+ "refs/tags/"
236+ } else {
237+ ""
238+ } ,
239+ r. name
240+ )
241+ . yellow ( )
242+ . to_string ( ) ,
243+ ReferenceKind :: Remote => format ! (
244+ "{}{}" ,
245+ if decorate_option == DecorateOptions :: Full {
246+ "refs/remotes/"
247+ } else {
248+ ""
249+ } ,
250+ r. name
251+ )
252+ . red ( )
253+ . to_string ( ) ,
254+ ReferenceKind :: Local => format ! (
255+ "{}{}" ,
256+ if decorate_option == DecorateOptions :: Full {
257+ "refs/heads/"
258+ } else {
259+ ""
260+ } ,
261+ r. name
262+ )
263+ . green ( )
264+ . to_string ( ) ,
265+ } )
266+ }
267+ } )
268+ . collect ( ) ,
269+ ) ;
270+ ref_msgs. join ( ", " )
271+ } else {
272+ String :: new ( )
273+ } ;
104274
105275 // prepare pathspecs for diff if needed
106276 let paths: Vec < PathBuf > = args. pathspec . iter ( ) . map ( util:: to_workdir_path) . collect ( ) ;
107277
108278 let message = if args. oneline {
109- // Oneline format: <short_hash> <commit_message_first_line>
279+ // Oneline format: <short_hash> <refs> < commit_message_first_line>
110280 let short_hash = & commit. id . to_string ( ) [ ..7 ] ;
111281 let ( msg, _) = parse_commit_msg ( & commit. message ) ;
112- if !branches. is_empty ( ) {
113- let branch_info = format ! ( " ({})" , branches. join( ", " ) ) ;
114- format ! (
115- "{} {}{}" ,
116- short_hash. yellow( ) . bold( ) ,
117- msg,
118- branch_info. green( )
119- )
282+ if !ref_msg. is_empty ( ) {
283+ format ! ( "{} ({}) {}" , short_hash. yellow( ) , ref_msg, msg)
120284 } else {
121285 format ! ( "{} {}" , short_hash. yellow( ) , msg)
122286 }
123287 } else {
124288 // Default detailed format
125- let mut message = format ! (
126- "{} {}" ,
127- "commit" . yellow( ) ,
128- if !branches. is_empty( ) {
129- commit. id. to_string( ) . yellow( ) . bold( )
130- } else {
131- commit. id. to_string( ) . yellow( )
132- }
133- ) ;
134-
135- // Show HEAD and branch info
136- if output_number == 1 {
137- // For the first commit (HEAD), show HEAD info and all branches
138- let mut refs = vec ! [ ] ;
139- let current_branch = if let Head :: Branch ( name) = head. to_owned ( ) {
140- refs. push ( format ! ( "{} -> {}" , "HEAD" . blue( ) , name. green( ) ) ) ;
141- Some ( name)
142- } else {
143- refs. push ( "HEAD" . blue ( ) . to_string ( ) ) ;
144- None
145- } ;
146-
147- // Add other branches pointing to this commit (excluding current branch)
148- let other_branches: Vec < String > = branches
149- . iter ( )
150- . filter ( |& b| current_branch. as_ref ( ) != Some ( b) )
151- . map ( |b| b. green ( ) . to_string ( ) )
152- . collect ( ) ;
153-
154- refs. extend ( other_branches) ;
155-
156- let ref_info = format ! ( " ({})" , refs. join( ", " ) ) ;
157- message = format ! ( "{message}{ref_info}" ) ;
158- } else if !branches. is_empty ( ) {
159- // Show branch info for other commits that are branch heads
160- let branch_info = format ! ( " ({})" , branches. join( ", " ) ) ;
161- message = format ! ( "{}{}" , message, branch_info. green( ) ) ;
162- }
289+ let mut message = if !ref_msg. is_empty ( ) {
290+ format ! (
291+ "{} {} ({})" ,
292+ "commit" . yellow( ) ,
293+ commit. id. to_string( ) . yellow( ) ,
294+ ref_msg
295+ )
296+ } else {
297+ format ! ( "{} {}" , "commit" . yellow( ) , commit. id. to_string( ) . yellow( ) )
298+ } ;
163299
164300 message. push_str ( & format ! ( "\n Author: {}" , commit. author) ) ;
165301 let ( msg, _) = parse_commit_msg ( & commit. message ) ;
@@ -192,24 +328,43 @@ pub async fn execute(args: LogArgs) {
192328 }
193329}
194330
195- /// Create a map of commit hashes to branch names
196- async fn create_branch_commits_map ( ) -> HashMap < SHA1 , Vec < String > > {
197- let all_branches = Branch :: list_branches ( None ) . await ;
198- let mut commit_to_branches: HashMap < SHA1 , Vec < String > > = HashMap :: new ( ) ;
331+ async fn create_reference_commit_map ( ) -> HashMap < SHA1 , Vec < Reference > > {
332+ let mut commit_to_refs: HashMap < SHA1 , Vec < Reference > > = HashMap :: new ( ) ;
199333
334+ let all_branches = Branch :: list_branches ( None ) . await ;
200335 for branch in all_branches {
201- let branch_name = match & branch. remote {
202- Some ( remote) => format ! ( "{}/{}" , remote, branch. name) ,
203- None => branch. name ,
204- } ;
205-
206- commit_to_branches
336+ commit_to_refs
207337 . entry ( branch. commit )
208338 . or_default ( )
209- . push ( branch_name) ;
339+ . push ( match & branch. remote {
340+ Some ( remote) => Reference {
341+ name : format ! ( "{}/{}" , remote, branch. name) ,
342+ kind : ReferenceKind :: Remote ,
343+ } ,
344+ None => Reference {
345+ name : branch. name ,
346+ kind : ReferenceKind :: Local ,
347+ } ,
348+ } ) ;
349+ }
350+
351+ let all_tags = crate :: internal:: tag:: list ( ) . await . expect ( "fatal: " ) ;
352+ for tag in all_tags {
353+ let commit_id = match tag. object {
354+ crate :: internal:: tag:: TagObject :: Commit ( c) => c. id ,
355+ crate :: internal:: tag:: TagObject :: Tag ( t) => t. object_hash ,
356+ _ => continue ,
357+ } ;
358+ commit_to_refs
359+ . entry ( commit_id)
360+ . or_default ( )
361+ . push ( Reference {
362+ name : tag. name ,
363+ kind : ReferenceKind :: Tag ,
364+ } ) ;
210365 }
211366
212- commit_to_branches
367+ commit_to_refs
213368}
214369
215370/// Generate unified diff between commit and its first parent (or empty tree)
0 commit comments