135212de Andy McCall 1 month ago ↶
1
2
3
4
5
6
7
8
9
10
use super :: * ;
11
use std:: time:: UNIX_EPOCH ;
12
13
# [ derive ( Debug, Clone, Default, PartialEq, Eq ) ]
14
pub struct PromptInfo {
15
pub bookmark : String,
16
17
pub change_id : String,
18
pub heads : usize ,
19
pub dirty : bool ,
20
pub conflicted : bool ,
21
22
pub ahead : Option< usize > ,
23
pub behind : Option< usize > ,
24
}
25
26
impl PromptInfo {
694805cc Andy McCall 1 month ago ↶
27
28
29
pub fn state_glyph ( & self ) -> & 'static str {
30
if self .conflicted {
31
" ‼"
32
} else if self .dirty {
33
" ✎"
34
} else {
35
" ✔"
36
}
37
}
38
39
40
pub fn state_color ( & self ) -> u8 {
41
if self .conflicted {
42
31
43
} else if self .dirty {
44
33
45
} else {
46
36
47
}
48
}
49
50
51
52
pub fn format_colored ( & self , fmt : & str , guard : ( &str , &str ) ) -> String {
53
format! ( " {} \x1b [{} m{} {} {} \x1b [0m{} " , guard.0 , self .state_color ( ) , guard.1 , self .format ( fmt) , guard.0 , guard.1 )
54
}
55
135212de Andy McCall 1 month ago ↶
56
57
694805cc Andy McCall 1 month ago ↶
58
59
60
135212de Andy McCall 1 month ago ↶
61
pub fn format ( & self , fmt : & str ) -> String {
62
let mut out = String :: new( ) ;
63
let mut chars = fmt.chars ( ) ;
64
while let Some ( c) = chars.next ( ) {
65
if c ! = ' %' {
66
out.push ( c) ;
67
continue ;
68
}
69
match chars.next ( ) {
70
Some ( ' b' ) => out.push_str ( & self .bookmark) ,
71
Some ( ' c' ) => out.push_str ( & self .change_id) ,
72
Some ( ' h' ) => out.push_str ( & self .heads.to_string ( ) ) ,
73
Some ( ' d' ) => {
74
if self .dirty {
75
out.push ( ' *' ) ;
76
}
77
}
78
Some ( ' x' ) => {
79
if self .conflicted {
80
out.push ( ' !' ) ;
81
}
82
}
83
Some ( ' a' ) => out.push_str ( & self .ahead.map ( | n | n.to_string ( ) ) .unwrap_or_default ( ) ) ,
84
Some ( ' B' ) => out.push_str ( & self .behind.map ( | n | n.to_string ( ) ) .unwrap_or_default ( ) ) ,
85
Some ( ' A' ) => {
86
if let Some ( n) = self .ahead.filter ( | n | * n > 0 ) {
87
out.push_str ( & format! ( " +{n} " ) ) ;
88
}
89
}
90
Some ( ' D' ) => {
91
if let Some ( n) = self .behind.filter ( | n | * n > 0 ) {
92
out.push_str ( & format! ( " -{n} " ) ) ;
93
}
94
}
694805cc Andy McCall 1 month ago ↶
95
Some ( ' s' ) => out.push_str ( self .state_glyph ( ) ) ,
96
Some ( ' U' ) => {
97
if let Some ( n) = self .ahead.filter ( | n | * n > 0 ) {
98
out.push_str ( & format! ( " ↑{n} " ) ) ;
99
}
100
}
101
Some ( ' O' ) => {
102
if let Some ( n) = self .behind.filter ( | n | * n > 0 ) {
103
out.push_str ( & format! ( " ↓{n} " ) ) ;
104
}
105
}
106
Some ( ' i' ) => out.push_str ( ICON ) ,
135212de Andy McCall 1 month ago ↶
107
Some ( ' %' ) => out.push ( ' %' ) ,
108
Some ( other) => {
109
out.push ( ' %' ) ;
110
out.push ( other) ;
111
}
112
None => out.push ( ' %' ) ,
113
}
114
}
115
out
116
}
117
}
118
119
b7f102fc Andy McCall 1 month ago ↶
120
121
122
# [ derive ( Clone, Debug ) ]
123
pub ( crate ) struct ManifestEntry {
124
pub size : u64 ,
125
pub mtime_ns : u128,
126
pub hash : String,
127
pub blob : Option< ObjectRef> ,
128
}
129
130
# [ derive ( Clone, Debug, Default ) ]
131
pub ( crate ) struct Manifest {
132
133
pub written_ns : u128,
134
pub entries : BTreeMap< String, ManifestEntry> ,
135
}
135212de Andy McCall 1 month ago ↶
136
b7f102fc Andy McCall 1 month ago ↶
137
pub ( crate ) fn mtime_ns ( m : & fs:: Metadata) -> u128 {
138
m.modified ( ) .ok ( ) .and_then ( | t | t.duration_since ( UNIX_EPOCH ) .ok ( ) ) .map ( | d | d.as_nanos ( ) ) .unwrap_or ( 0 )
139
}
140
135212de Andy McCall 1 month ago ↶
141
pub const DEFAULT_PROMPT_FORMAT : & str = " %b %c%d%x%A%D" ;
694805cc Andy McCall 1 month ago ↶
142
143
pub const ICON : & str = " ⚛" ;
144
145
pub const GLYPH_PROMPT_FORMAT : & str = " %i %b %c %s%U%O" ;
135212de Andy McCall 1 month ago ↶
146
b7f102fc Andy McCall 1 month ago ↶
147
148
149
fn manifest_line ( path : & str , size : u64 , mtime_ns : u128, hash : & str , blob : Option< ObjectRef> ) -> String {
150
format! ( " {size} \t {mtime_ns} \t {hash} \t {} \t {path} \n " , blob.map ( | b | b.to_hex ( ) ) .unwrap_or_else ( | | " -" .into ( ) ) )
135212de Andy McCall 1 month ago ↶
151
}
152
153
impl Repo {
154
fn manifest_path ( & self ) -> PathBuf {
155
self .oxy.join ( " workspaces" ) .join ( self .workspace.to_hex ( ) ) .join ( " manifest" )
156
}
157
158
159
160
161
pub ( crate ) fn write_manifest ( & self , conflicted : bool , uncommitted : bool ) -> Result< ( ) > {
b7f102fc Andy McCall 1 month ago ↶
162
let entries = self .walk_working_copy ( ) ? ;
163
self .write_manifest_entries ( conflicted, uncommitted, & entries, & HashMap:: new( ) )
164
}
165
166
167
168
pub ( crate ) fn write_manifest_entries ( & self , conflicted : bool , uncommitted : bool , entries : & BTreeMap< String, FsEntry> , blobs : & HashMap< String, ObjectRef> ) -> Result< ( ) > {
169
let written = std:: time:: SystemTime:: now( ) .duration_since ( UNIX_EPOCH ) .map ( | d | d.as_nanos ( ) ) .unwrap_or ( 0 ) ;
170
let mut text = format! ( " # conflicted={} \n # uncommitted={} \n # written={written} \n " , conflicted as u8 , uncommitted as u8 ) ;
171
for ( path, e) in entries {
172
let full = self .root.join ( path) ;
135212de Andy McCall 1 month ago ↶
173
let ( size, mtime_ns) = match fs:: symlink_metadata( & full) {
b7f102fc Andy McCall 1 month ago ↶
174
Ok ( m) => ( m.len ( ) , mtime_ns ( & m) ) ,
135212de Andy McCall 1 month ago ↶
175
Err ( _ ) => ( e.content.len ( ) as u64 , 0 ) ,
176
} ;
b7f102fc Andy McCall 1 month ago ↶
177
let hash = match & e.content_hash {
178
Some ( h) => h.clone ( ) ,
179
None => blake3:: hash( & e.content) .to_hex ( ) .to_string ( ) ,
180
} ;
181
let blob = blobs.get ( path) .copied ( ) .or ( e.blob) ;
182
text.push_str ( & manifest_line ( path, size, mtime_ns, & hash, blob) ) ;
135212de Andy McCall 1 month ago ↶
183
}
184
let p = self .manifest_path ( ) ;
185
fs:: create_dir_all( p.parent ( ) .unwrap ( ) ) ? ;
186
write_atomic ( & p, text.as_bytes ( ) )
187
}
188
b7f102fc Andy McCall 1 month ago ↶
189
190
191
pub ( crate ) fn rewrite_manifest_flags ( & self , conflicted : bool , uncommitted : bool ) -> Result< ( ) > {
192
let p = self .manifest_path ( ) ;
193
let text = match fs:: read_to_string( & p) {
194
Ok ( t) => t,
195
Err ( _ ) => return self .write_manifest ( conflicted, uncommitted) ,
196
} ;
197
let mut out = String :: with_capacity( text.len ( ) ) ;
198
for line in text.lines ( ) {
199
if line.starts_with ( " # conflicted=" ) {
200
out.push_str ( & format! ( " # conflicted={} \n " , conflicted as u8 ) ) ;
201
} else if line.starts_with ( " # uncommitted=" ) {
202
out.push_str ( & format! ( " # uncommitted={} \n " , uncommitted as u8 ) ) ;
203
} else {
204
out.push_str ( line) ;
205
out.push ( ' \n ' ) ;
206
}
207
}
208
write_atomic ( & p, out.as_bytes ( ) )
209
}
210
135212de Andy McCall 1 month ago ↶
211
212
213
214
pub ( crate ) fn drop_manifest ( & self ) {
215
let _ = fs:: remove_file( self .manifest_path ( ) ) ;
216
}
217
b7f102fc Andy McCall 1 month ago ↶
218
pub ( crate ) fn read_manifest ( & self ) -> Result< Option< ( bool , bool , Manifest) > > {
135212de Andy McCall 1 month ago ↶
219
let text = match fs:: read_to_string( self .manifest_path ( ) ) {
220
Ok ( t) => t,
221
Err ( e) if e.kind ( ) = = std:: io:: ErrorKind:: NotFound => return Ok ( None ) ,
222
Err ( e) => return Err ( e.into ( ) ) ,
223
} ;
b7f102fc Andy McCall 1 month ago ↶
224
let mut out = Manifest:: default( ) ;
135212de Andy McCall 1 month ago ↶
225
let mut conflicted = false ;
226
let mut uncommitted = false ;
227
for line in text.lines ( ) {
228
if let Some ( v) = line.strip_prefix ( " # conflicted=" ) {
229
conflicted = v.trim ( ) = = " 1" ;
230
continue ;
231
}
232
if let Some ( v) = line.strip_prefix ( " # uncommitted=" ) {
233
uncommitted = v.trim ( ) = = " 1" ;
234
continue ;
235
}
b7f102fc Andy McCall 1 month ago ↶
236
if let Some ( v) = line.strip_prefix ( " # written=" ) {
237
out.written_ns = v.trim ( ) .parse ( ) .unwrap_or ( u128:: MAX ) ;
238
continue ;
239
}
240
let f: Vec< & str > = line.splitn ( 5 , ' \t ' ) .collect ( ) ;
241
if f.len ( ) = = 5 {
242
out.entries.insert (
243
f[ 4 ] .to_string ( ) ,
244
ManifestEntry { size: f[ 0 ] .parse ( ) .unwrap_or ( 0 ) , mtime_ns: f[ 1 ] .parse ( ) .unwrap_or ( 0 ) , hash: f[ 2 ] .to_string ( ) , blob: ObjectRef:: from_hex( f[ 3 ] ) } ,
245
) ;
135212de Andy McCall 1 month ago ↶
246
}
247
}
248
Ok ( Some ( ( conflicted, uncommitted, out) ) )
249
}
250
251
252
253
pub fn quick_dirty ( & self ) -> Result< Option< bool > > {
254
let Some ( ( _ , uncommitted, manifest) ) = self .read_manifest ( ) ? else { return Ok ( None ) } ;
255
if uncommitted {
256
return Ok ( Some ( true ) ) ;
257
}
258
let mut seen = 0 usize ;
4dee277c Andy McCall 1 month ago ↶
259
let nested = std:: sync:: Arc:: new( std:: sync:: Mutex:: new( Vec :: new( ) ) ) ;
260
let wb = self .walker ( & nested) ;
135212de Andy McCall 1 month ago ↶
261
for entry in wb.build ( ) {
262
let Ok ( entry) = entry else { continue } ;
263
let Some ( ft) = entry.file_type ( ) else { continue } ;
264
if ft.is_dir ( ) {
265
continue ;
266
}
267
let rel: String = entry.path ( ) .strip_prefix ( & self .root) .unwrap ( ) .components ( ) .map ( | c | c.as_os_str ( ) .to_string_lossy ( ) .to_string ( ) ) .collect:: < Vec< _ > > ( ) .join ( " /" ) ;
7421309f Andy McCall 1 month ago ↶
268
if ! self .in_scope ( & rel) {
269
continue ;
270
}
b7f102fc Andy McCall 1 month ago ↶
271
let Some ( me) = manifest.entries.get ( & rel) else { return Ok ( Some ( true ) ) } ;
135212de Andy McCall 1 month ago ↶
272
seen + = 1 ;
273
let m = fs:: symlink_metadata( entry.path ( ) ) ? ;
b7f102fc Andy McCall 1 month ago ↶
274
let now_mtime = mtime_ns ( & m) ;
135212de Andy McCall 1 month ago ↶
275
if ft.is_symlink ( ) {
276
let target = fs:: read_link( entry.path ( ) ) ? ;
b7f102fc Andy McCall 1 month ago ↶
277
if blake3:: hash( target.to_string_lossy ( ) .as_bytes ( ) ) .to_hex ( ) .as_str ( ) ! = me.hash {
135212de Andy McCall 1 month ago ↶
278
return Ok ( Some ( true ) ) ;
279
}
280
continue ;
281
}
b7f102fc Andy McCall 1 month ago ↶
282
if m.len ( ) ! = me.size {
135212de Andy McCall 1 month ago ↶
283
return Ok ( Some ( true ) ) ;
284
}
b7f102fc Andy McCall 1 month ago ↶
285
if now_mtime ! = me.mtime_ns & & blake3:: hash( & fs:: read( entry.path ( ) ) ? ) .to_hex ( ) .as_str ( ) ! = me.hash {
135212de Andy McCall 1 month ago ↶
286
return Ok ( Some ( true ) ) ;
287
}
288
}
9ac1fbe2 Andy McCall 1 month ago ↶
289
if seen = = manifest.entries.len ( ) {
290
return Ok ( Some ( false ) ) ;
291
}
292
293
294
for ( rel, me) in & manifest.entries {
295
let path = self .root.join ( rel) ;
296
let Ok ( m) = fs:: symlink_metadata( & path) else { return Ok ( Some ( true ) ) } ;
297
if m.file_type ( ) .is_symlink ( ) {
298
continue ;
299
}
300
if m.len ( ) ! = me.size | | ( mtime_ns ( & m) ! = me.mtime_ns & & blake3:: hash( & fs:: read( & path) ? ) .to_hex ( ) .as_str ( ) ! = me.hash) {
301
return Ok ( Some ( true ) ) ;
302
}
303
}
304
Ok ( Some ( false ) )
135212de Andy McCall 1 month ago ↶
305
}
306
307
308
pub fn quick_status ( & self ) -> Result< PromptInfo> {
309
let view = self .view ( ) ? ;
310
let bookmark = self .config.bookmark.clone ( ) ;
311
let heads = self .heads_of ( & view, & bookmark) ? ;
312
let wc_hash = view.workspaces.get ( & self .workspace) .copied ( ) ;
313
let change_id = match wc_hash {
314
Some ( h) => self .store.get_change ( & h) ? .change_id.short ( 8 ) ,
315
None => String :: new( ) ,
316
} ;
317
318
319
let manifest = self .read_manifest ( ) ? ;
320
let conflicted = manifest.as_ref ( ) .map ( | ( c , _, _) | * c) .unwrap_or ( false ) ;
321
let dirty = self .quick_dirty ( ) ? .unwrap_or ( false ) ;
322
let ( ahead, behind) = match view.remotes.get ( " origin" ) .and_then ( | b | b.get ( & bookmark) ) {
323
Some ( st) => {
324
let remote_heads = self .store.get_state ( st) ? .heads;
325
let ahead = self .negotiate ( & remote_heads, & heads) .map ( | v | v.len ( ) ) .ok ( ) ;
326
let behind = self .negotiate ( & heads, & remote_heads) .map ( | v | v.len ( ) ) .ok ( ) ;
327
( ahead, behind)
328
}
329
None => ( None , None ) ,
330
} ;
331
Ok ( PromptInfo { bookmark, change_id, heads: heads.len ( ) , dirty, conflicted, ahead, behind } )
332
}
333
}
694805cc Andy McCall 1 month ago ↶
334
335
# [ cfg ( test ) ]
336
mod tests {
337
use super :: * ;
338
339
# [ test ]
340
fn placeholders_and_glyphs ( ) {
341
let clean = PromptInfo { bookmark: " main" .into ( ) , change_id: " 658990aa" .into ( ) , heads: 1 , dirty: false , conflicted: false , ahead: Some ( 0 ) , behind: None } ;
342
assert_eq! ( clean.format ( DEFAULT_PROMPT_FORMAT ) , " main 658990aa" ) ;
343
assert_eq! ( clean.format ( GLYPH_PROMPT_FORMAT ) , " ⚛ main 658990aa ✔" ) ;
344
let busy = PromptInfo { dirty: true , ahead: Some ( 2 ) , behind: Some ( 3 ) , .. clean.clone ( ) } ;
345
assert_eq! ( busy.format ( DEFAULT_PROMPT_FORMAT ) , " main 658990aa*+2-3" ) ;
346
assert_eq! ( busy.format ( GLYPH_PROMPT_FORMAT ) , " ⚛ main 658990aa ✎↑2↓3" ) ;
347
let bad = PromptInfo { conflicted: true , .. busy.clone ( ) } ;
348
assert_eq! ( bad.format ( " %s%x%d %h %a/%B %%" ) , " ‼!* 1 2/3 %" ) ;
349
assert_eq! ( bad.state_color ( ) , 31 ) ;
350
assert_eq! ( busy.format_colored ( " %b" , ( " \\ [" , " \\ ]" ) ) , " \\ [\x1b [33m\\ ]main\\ [\x1b [0m\\ ]" ) ;
351
assert_eq! ( clean.format_colored ( " %b" , ( " " , " " ) ) , " \x1b [36mmain\x1b [0m" ) ;
352
assert_eq! ( clean.format ( " %q" ) , " %q" ) ;
353
}
354
}
355