@@ -5,6 +5,7 @@ pub struct Options {
5
5
pub attributes : Option < Attributes > ,
6
6
pub statistics : bool ,
7
7
pub simple : bool ,
8
+ pub recurse_submodules : bool ,
8
9
}
9
10
10
11
#[ derive( Debug , Copy , Clone ) ]
@@ -16,14 +17,17 @@ pub enum Attributes {
16
17
}
17
18
18
19
pub ( crate ) mod function {
19
- use gix:: bstr:: BString ;
20
+ use gix:: bstr:: { BStr , BString } ;
20
21
use std:: collections:: BTreeSet ;
21
22
use std:: {
22
23
borrow:: Cow ,
23
24
io:: { BufWriter , Write } ,
24
25
} ;
25
26
27
+ use crate :: OutputFormat ;
26
28
use gix:: odb:: FindExt ;
29
+ use gix:: repository:: IndexPersistedOrInMemory ;
30
+ use gix:: Repository ;
27
31
28
32
use crate :: repository:: index:: entries:: { Attributes , Options } ;
29
33
@@ -37,59 +41,83 @@ pub(crate) mod function {
37
41
format,
38
42
attributes,
39
43
statistics,
44
+ recurse_submodules,
40
45
} : Options ,
41
46
) -> anyhow:: Result < ( ) > {
42
47
use crate :: OutputFormat :: * ;
43
- let index = repo. index_or_load_from_head ( ) ?;
44
- let pathspec = repo. pathspec (
45
- pathspecs,
46
- false ,
47
- & index,
48
- gix:: worktree:: stack:: state:: attributes:: Source :: WorktreeThenIdMapping . adjust_for_bare ( repo. is_bare ( ) ) ,
48
+ let mut out = BufWriter :: with_capacity ( 64 * 1024 , out) ;
49
+ let mut all_attrs = statistics. then ( BTreeSet :: new) ;
50
+
51
+ #[ cfg( feature = "serde" ) ]
52
+ if let Json = format {
53
+ out. write_all ( b"[\n " ) ?;
54
+ }
55
+
56
+ let stats = print_entries (
57
+ & repo,
58
+ attributes,
59
+ pathspecs. iter ( ) ,
60
+ format,
61
+ all_attrs. as_mut ( ) ,
62
+ simple,
63
+ "" . into ( ) ,
64
+ recurse_submodules,
65
+ & mut out,
49
66
) ?;
50
- let mut cache = attributes
51
- . or_else ( || {
52
- pathspec
53
- . search ( )
54
- . patterns ( )
55
- . any ( |spec| !spec. attributes . is_empty ( ) )
56
- . then_some ( Attributes :: Index )
57
- } )
58
- . map ( |attrs| {
59
- repo. attributes (
60
- & index,
61
- match attrs {
62
- Attributes :: WorktreeAndIndex => {
63
- gix:: worktree:: stack:: state:: attributes:: Source :: WorktreeThenIdMapping
64
- . adjust_for_bare ( repo. is_bare ( ) )
65
- }
66
- Attributes :: Index => gix:: worktree:: stack:: state:: attributes:: Source :: IdMapping ,
67
- } ,
68
- match attrs {
69
- Attributes :: WorktreeAndIndex => {
70
- gix:: worktree:: stack:: state:: ignore:: Source :: WorktreeThenIdMappingIfNotSkipped
71
- . adjust_for_bare ( repo. is_bare ( ) )
72
- }
73
- Attributes :: Index => gix:: worktree:: stack:: state:: ignore:: Source :: IdMapping ,
74
- } ,
75
- None ,
76
- )
77
- . map ( |cache| ( cache. attribute_matches ( ) , cache) )
67
+
68
+ #[ cfg( feature = "serde" ) ]
69
+ if format == Json {
70
+ out. write_all ( b"]\n " ) ?;
71
+ out. flush ( ) ?;
72
+ if statistics {
73
+ serde_json:: to_writer_pretty ( & mut err, & stats) ?;
74
+ }
75
+ } else if format == Human && statistics {
76
+ out. flush ( ) ?;
77
+ writeln ! ( err, "{stats:#?}" ) ?;
78
+ if let Some ( attrs) = all_attrs. filter ( |a| !a. is_empty ( ) ) {
79
+ writeln ! ( err, "All encountered attributes:" ) ?;
80
+ for attr in attrs {
81
+ writeln ! ( err, "\t {attr}" , attr = attr. as_ref( ) ) ?;
82
+ }
83
+ }
84
+ }
85
+ Ok ( ( ) )
86
+ }
87
+
88
+ #[ allow( clippy:: too_many_arguments) ]
89
+ fn print_entries (
90
+ repo : & Repository ,
91
+ attributes : Option < Attributes > ,
92
+ pathspecs : impl IntoIterator < Item = impl AsRef < BStr > > + Clone ,
93
+ format : OutputFormat ,
94
+ mut all_attrs : Option < & mut BTreeSet < gix:: attrs:: Assignment > > ,
95
+ simple : bool ,
96
+ prefix : & BStr ,
97
+ recurse_submodules : bool ,
98
+ out : & mut impl std:: io:: Write ,
99
+ ) -> anyhow:: Result < Statistics > {
100
+ let ( mut pathspec, index, mut cache) = init_cache ( repo, attributes, pathspecs. clone ( ) ) ?;
101
+ let submodules_by_path = recurse_submodules
102
+ . then ( || {
103
+ repo. submodules ( )
104
+ . map ( |opt| {
105
+ opt. map ( |submodules| {
106
+ submodules
107
+ . map ( |sm| sm. path ( ) . map ( Cow :: into_owned) . map ( move |path| ( path, sm) ) )
108
+ . collect :: < Result < Vec < _ > , _ > > ( )
109
+ } )
110
+ } )
111
+ . transpose ( )
78
112
} )
113
+ . flatten ( )
114
+ . transpose ( ) ?
79
115
. transpose ( ) ?;
80
116
let mut stats = Statistics {
81
117
entries : index. entries ( ) . len ( ) ,
82
118
..Default :: default ( )
83
119
} ;
84
-
85
- let mut out = BufWriter :: with_capacity ( 64 * 1024 , out) ;
86
- #[ cfg( feature = "serde" ) ]
87
- if let Json = format {
88
- out. write_all ( b"[\n " ) ?;
89
- }
90
- let ( mut search, _cache) = pathspec. into_parts ( ) ;
91
- let mut all_attrs = statistics. then ( BTreeSet :: new) ;
92
- if let Some ( entries) = index. prefixed_entries ( search. common_prefix ( ) ) {
120
+ if let Some ( entries) = index. prefixed_entries ( pathspec. common_prefix ( ) ) {
93
121
stats. entries_after_prune = entries. len ( ) ;
94
122
let mut entries = entries. iter ( ) . peekable ( ) ;
95
123
while let Some ( entry) = entries. next ( ) {
@@ -110,7 +138,7 @@ pub(crate) mod function {
110
138
} ;
111
139
stats. with_attributes += usize:: from ( !attributes. is_empty ( ) ) ;
112
140
stats. max_attributes_per_path = stats. max_attributes_per_path . max ( attributes. len ( ) ) ;
113
- if let Some ( attrs) = all_attrs. as_mut ( ) {
141
+ if let Some ( attrs) = all_attrs. as_deref_mut ( ) {
114
142
attributes. iter ( ) . for_each ( |attr| {
115
143
attrs. insert ( attr. clone ( ) ) ;
116
144
} ) ;
@@ -126,7 +154,7 @@ pub(crate) mod function {
126
154
127
155
// Note that we intentionally ignore `_case` so that we act like git does, attribute matching case is determined
128
156
// by the repository, not the pathspec.
129
- if search
157
+ let entry_is_excluded = pathspec
130
158
. pattern_matching_relative_path ( entry. path ( & index) , Some ( false ) , |rela_path, _case, is_dir, out| {
131
159
cache
132
160
. as_mut ( )
@@ -147,44 +175,110 @@ pub(crate) mod function {
147
175
} )
148
176
. unwrap_or_default ( )
149
177
} )
150
- . map_or ( true , |m| m. is_excluded ( ) )
151
- {
178
+ . map_or ( true , |m| m. is_excluded ( ) ) ;
179
+
180
+ let entry_is_submodule = entry. mode . is_submodule ( ) ;
181
+ if entry_is_excluded && ( !entry_is_submodule || !recurse_submodules) {
152
182
continue ;
153
183
}
154
- match format {
155
- Human => {
156
- if simple {
157
- to_human_simple ( & mut out, & index, entry, attrs)
158
- } else {
159
- to_human ( & mut out, & index, entry, attrs)
160
- } ?
184
+ if let Some ( sm) = submodules_by_path
185
+ . as_ref ( )
186
+ . filter ( |_| entry_is_submodule)
187
+ . and_then ( |sms_by_path| {
188
+ let entry_path = entry. path ( & index) ;
189
+ sms_by_path
190
+ . iter ( )
191
+ . find_map ( |( path, sm) | ( path == entry_path) . then_some ( sm) )
192
+ . filter ( |sm| sm. git_dir_try_old_form ( ) . map_or ( false , |dot_git| dot_git. exists ( ) ) )
193
+ } )
194
+ {
195
+ let sm_path = gix:: path:: to_unix_separators_on_windows ( sm. path ( ) ?) ;
196
+ let sm_repo = sm. open ( ) ?. expect ( "we checked it exists" ) ;
197
+ let mut prefix = prefix. to_owned ( ) ;
198
+ prefix. extend_from_slice ( sm_path. as_ref ( ) ) ;
199
+ if !sm_path. ends_with ( b"/" ) {
200
+ prefix. push ( b'/' ) ;
161
201
}
162
- #[ cfg( feature = "serde" ) ]
163
- Json => to_json ( & mut out, & index, entry, attrs, entries. peek ( ) . is_none ( ) ) ?,
164
- }
165
- }
166
-
167
- #[ cfg( feature = "serde" ) ]
168
- if format == Json {
169
- out. write_all ( b"]\n " ) ?;
170
- out. flush ( ) ?;
171
- if statistics {
172
- serde_json:: to_writer_pretty ( & mut err, & stats) ?;
173
- }
174
- }
175
- if format == Human && statistics {
176
- out. flush ( ) ?;
177
- stats. cache = cache. map ( |c| * c. 1 . statistics ( ) ) ;
178
- writeln ! ( err, "{stats:#?}" ) ?;
179
- if let Some ( attrs) = all_attrs. filter ( |a| !a. is_empty ( ) ) {
180
- writeln ! ( err, "All encountered attributes:" ) ?;
181
- for attr in attrs {
182
- writeln ! ( err, "\t {attr}" , attr = attr. as_ref( ) ) ?;
202
+ let sm_stats = print_entries (
203
+ & sm_repo,
204
+ attributes,
205
+ pathspecs. clone ( ) ,
206
+ format,
207
+ all_attrs. as_deref_mut ( ) ,
208
+ simple,
209
+ prefix. as_ref ( ) ,
210
+ recurse_submodules,
211
+ out,
212
+ ) ?;
213
+ stats. submodule . push ( ( sm_path. into_owned ( ) , sm_stats) ) ;
214
+ } else {
215
+ match format {
216
+ OutputFormat :: Human => {
217
+ if simple {
218
+ to_human_simple ( out, & index, entry, attrs, prefix)
219
+ } else {
220
+ to_human ( out, & index, entry, attrs, prefix)
221
+ } ?
222
+ }
223
+ #[ cfg( feature = "serde" ) ]
224
+ OutputFormat :: Json => to_json ( out, & index, entry, attrs, entries. peek ( ) . is_none ( ) , prefix) ?,
183
225
}
184
226
}
185
227
}
186
228
}
187
- Ok ( ( ) )
229
+
230
+ stats. cache = cache. map ( |c| * c. 1 . statistics ( ) ) ;
231
+ Ok ( stats)
232
+ }
233
+
234
+ #[ allow( clippy:: type_complexity) ]
235
+ fn init_cache (
236
+ repo : & Repository ,
237
+ attributes : Option < Attributes > ,
238
+ pathspecs : impl IntoIterator < Item = impl AsRef < BStr > > ,
239
+ ) -> anyhow:: Result < (
240
+ gix:: pathspec:: Search ,
241
+ IndexPersistedOrInMemory ,
242
+ Option < ( gix:: attrs:: search:: Outcome , gix:: worktree:: Stack ) > ,
243
+ ) > {
244
+ let index = repo. index_or_load_from_head ( ) ?;
245
+ let pathspec = repo. pathspec (
246
+ pathspecs,
247
+ false ,
248
+ & index,
249
+ gix:: worktree:: stack:: state:: attributes:: Source :: WorktreeThenIdMapping . adjust_for_bare ( repo. is_bare ( ) ) ,
250
+ ) ?;
251
+ let cache = attributes
252
+ . or_else ( || {
253
+ pathspec
254
+ . search ( )
255
+ . patterns ( )
256
+ . any ( |spec| !spec. attributes . is_empty ( ) )
257
+ . then_some ( Attributes :: Index )
258
+ } )
259
+ . map ( |attrs| {
260
+ repo. attributes (
261
+ & index,
262
+ match attrs {
263
+ Attributes :: WorktreeAndIndex => {
264
+ gix:: worktree:: stack:: state:: attributes:: Source :: WorktreeThenIdMapping
265
+ . adjust_for_bare ( repo. is_bare ( ) )
266
+ }
267
+ Attributes :: Index => gix:: worktree:: stack:: state:: attributes:: Source :: IdMapping ,
268
+ } ,
269
+ match attrs {
270
+ Attributes :: WorktreeAndIndex => {
271
+ gix:: worktree:: stack:: state:: ignore:: Source :: WorktreeThenIdMappingIfNotSkipped
272
+ . adjust_for_bare ( repo. is_bare ( ) )
273
+ }
274
+ Attributes :: Index => gix:: worktree:: stack:: state:: ignore:: Source :: IdMapping ,
275
+ } ,
276
+ None ,
277
+ )
278
+ . map ( |cache| ( cache. attribute_matches ( ) , cache) )
279
+ } )
280
+ . transpose ( ) ?;
281
+ Ok ( ( pathspec. into_parts ( ) . 0 , index, cache) )
188
282
}
189
283
190
284
#[ cfg_attr( feature = "serde" , derive( serde:: Serialize ) ) ]
@@ -203,6 +297,7 @@ pub(crate) mod function {
203
297
pub with_attributes : usize ,
204
298
pub max_attributes_per_path : usize ,
205
299
pub cache : Option < gix:: worktree:: stack:: Statistics > ,
300
+ pub submodule : Vec < ( BString , Statistics ) > ,
206
301
}
207
302
208
303
#[ cfg( feature = "serde" ) ]
@@ -212,6 +307,7 @@ pub(crate) mod function {
212
307
entry : & gix:: index:: Entry ,
213
308
attrs : Option < Attrs > ,
214
309
is_last : bool ,
310
+ prefix : & BStr ,
215
311
) -> anyhow:: Result < ( ) > {
216
312
use gix:: bstr:: ByteSlice ;
217
313
#[ derive( serde:: Serialize ) ]
@@ -231,7 +327,13 @@ pub(crate) mod function {
231
327
hex_id : entry. id . to_hex ( ) . to_string ( ) ,
232
328
flags : entry. flags . bits ( ) ,
233
329
mode : entry. mode . bits ( ) ,
234
- path : entry. path ( index) . to_str_lossy ( ) ,
330
+ path : if prefix. is_empty ( ) {
331
+ entry. path ( index) . to_str_lossy ( )
332
+ } else {
333
+ let mut path = prefix. to_owned ( ) ;
334
+ path. extend_from_slice ( entry. path ( index) ) ;
335
+ path. to_string ( ) . into ( )
336
+ } ,
235
337
meta : attrs,
236
338
} ,
237
339
) ?;
@@ -249,11 +351,15 @@ pub(crate) mod function {
249
351
file : & gix:: index:: File ,
250
352
entry : & gix:: index:: Entry ,
251
353
attrs : Option < Attrs > ,
354
+ prefix : & BStr ,
252
355
) -> std:: io:: Result < ( ) > {
356
+ if !prefix. is_empty ( ) {
357
+ out. write_all ( prefix) ?;
358
+ }
253
359
match attrs {
254
360
Some ( attrs) => {
255
361
out. write_all ( entry. path ( file) ) ?;
256
- out. write_all ( print_attrs ( Some ( attrs) ) . as_bytes ( ) )
362
+ out. write_all ( print_attrs ( Some ( attrs) , entry . mode ) . as_bytes ( ) )
257
363
}
258
364
None => out. write_all ( entry. path ( file) ) ,
259
365
} ?;
@@ -265,10 +371,11 @@ pub(crate) mod function {
265
371
file : & gix:: index:: File ,
266
372
entry : & gix:: index:: Entry ,
267
373
attrs : Option < Attrs > ,
374
+ prefix : & BStr ,
268
375
) -> std:: io:: Result < ( ) > {
269
376
writeln ! (
270
377
out,
271
- "{} {}{:?} {} {}{}" ,
378
+ "{} {}{:?} {} {}{}{} " ,
272
379
match entry. flags. stage( ) {
273
380
0 => "BASE " ,
274
381
1 => "OURS " ,
@@ -282,14 +389,20 @@ pub(crate) mod function {
282
389
} ,
283
390
entry. mode,
284
391
entry. id,
392
+ prefix,
285
393
entry. path( file) ,
286
- print_attrs( attrs)
394
+ print_attrs( attrs, entry . mode )
287
395
)
288
396
}
289
397
290
- fn print_attrs ( attrs : Option < Attrs > ) -> Cow < ' static , str > {
398
+ fn print_attrs ( attrs : Option < Attrs > , mode : gix :: index :: entry :: Mode ) -> Cow < ' static , str > {
291
399
attrs. map_or ( Cow :: Borrowed ( "" ) , |a| {
292
400
let mut buf = String :: new ( ) ;
401
+ if mode. is_sparse ( ) {
402
+ buf. push_str ( " 📁 " ) ;
403
+ } else if mode. is_submodule ( ) {
404
+ buf. push_str ( " ➡ " ) ;
405
+ }
293
406
if a. is_excluded {
294
407
buf. push_str ( " ❌" ) ;
295
408
}
0 commit comments