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 {
27
28
29
30
pub fn format ( & self , fmt : & str ) -> String {
31
let mut out = String :: new( ) ;
32
let mut chars = fmt.chars ( ) ;
33
while let Some ( c) = chars.next ( ) {
34
if c ! = ' %' {
35
out.push ( c) ;
36
continue ;
37
}
38
match chars.next ( ) {
39
Some ( ' b' ) => out.push_str ( & self .bookmark) ,
40
Some ( ' c' ) => out.push_str ( & self .change_id) ,
41
Some ( ' h' ) => out.push_str ( & self .heads.to_string ( ) ) ,
42
Some ( ' d' ) => {
43
if self .dirty {
44
out.push ( ' *' ) ;
45
}
46
}
47
Some ( ' x' ) => {
48
if self .conflicted {
49
out.push ( ' !' ) ;
50
}
51
}
52
Some ( ' a' ) => out.push_str ( & self .ahead.map ( | n | n.to_string ( ) ) .unwrap_or_default ( ) ) ,
53
Some ( ' B' ) => out.push_str ( & self .behind.map ( | n | n.to_string ( ) ) .unwrap_or_default ( ) ) ,
54
Some ( ' A' ) => {
55
if let Some ( n) = self .ahead.filter ( | n | * n > 0 ) {
56
out.push_str ( & format! ( " +{n} " ) ) ;
57
}
58
}
59
Some ( ' D' ) => {
60
if let Some ( n) = self .behind.filter ( | n | * n > 0 ) {
61
out.push_str ( & format! ( " -{n} " ) ) ;
62
}
63
}
64
Some ( ' %' ) => out.push ( ' %' ) ,
65
Some ( other) => {
66
out.push ( ' %' ) ;
67
out.push ( other) ;
68
}
69
None => out.push ( ' %' ) ,
70
}
71
}
72
out
73
}
74
}
75
76
b7f102fc Andy McCall 1 month ago ↶
77
78
79
# [ derive ( Clone, Debug ) ]
80
pub ( crate ) struct ManifestEntry {
81
pub size : u64 ,
82
pub mtime_ns : u128,
83
pub hash : String,
84
pub blob : Option< ObjectRef> ,
85
}
86
87
# [ derive ( Clone, Debug, Default ) ]
88
pub ( crate ) struct Manifest {
89
90
pub written_ns : u128,
91
pub entries : BTreeMap< String, ManifestEntry> ,
92
}
135212de Andy McCall 1 month ago ↶
93
b7f102fc Andy McCall 1 month ago ↶
94
pub ( crate ) fn mtime_ns ( m : & fs:: Metadata) -> u128 {
95
m.modified ( ) .ok ( ) .and_then ( | t | t.duration_since ( UNIX_EPOCH ) .ok ( ) ) .map ( | d | d.as_nanos ( ) ) .unwrap_or ( 0 )
96
}
97
135212de Andy McCall 1 month ago ↶
98
pub const DEFAULT_PROMPT_FORMAT : & str = " %b %c%d%x%A%D" ;
99
b7f102fc Andy McCall 1 month ago ↶
100
101
102
fn manifest_line ( path : & str , size : u64 , mtime_ns : u128, hash : & str , blob : Option< ObjectRef> ) -> String {
103
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 ↶
104
}
105
106
impl Repo {
107
fn manifest_path ( & self ) -> PathBuf {
108
self .oxy.join ( " workspaces" ) .join ( self .workspace.to_hex ( ) ) .join ( " manifest" )
109
}
110
111
112
113
114
pub ( crate ) fn write_manifest ( & self , conflicted : bool , uncommitted : bool ) -> Result< ( ) > {
b7f102fc Andy McCall 1 month ago ↶
115
let entries = self .walk_working_copy ( ) ? ;
116
self .write_manifest_entries ( conflicted, uncommitted, & entries, & HashMap:: new( ) )
117
}
118
119
120
121
pub ( crate ) fn write_manifest_entries ( & self , conflicted : bool , uncommitted : bool , entries : & BTreeMap< String, FsEntry> , blobs : & HashMap< String, ObjectRef> ) -> Result< ( ) > {
122
let written = std:: time:: SystemTime:: now( ) .duration_since ( UNIX_EPOCH ) .map ( | d | d.as_nanos ( ) ) .unwrap_or ( 0 ) ;
123
let mut text = format! ( " # conflicted={} \n # uncommitted={} \n # written={written} \n " , conflicted as u8 , uncommitted as u8 ) ;
124
for ( path, e) in entries {
125
let full = self .root.join ( path) ;
135212de Andy McCall 1 month ago ↶
126
let ( size, mtime_ns) = match fs:: symlink_metadata( & full) {
b7f102fc Andy McCall 1 month ago ↶
127
Ok ( m) => ( m.len ( ) , mtime_ns ( & m) ) ,
135212de Andy McCall 1 month ago ↶
128
Err ( _ ) => ( e.content.len ( ) as u64 , 0 ) ,
129
} ;
b7f102fc Andy McCall 1 month ago ↶
130
let hash = match & e.content_hash {
131
Some ( h) => h.clone ( ) ,
132
None => blake3:: hash( & e.content) .to_hex ( ) .to_string ( ) ,
133
} ;
134
let blob = blobs.get ( path) .copied ( ) .or ( e.blob) ;
135
text.push_str ( & manifest_line ( path, size, mtime_ns, & hash, blob) ) ;
135212de Andy McCall 1 month ago ↶
136
}
137
let p = self .manifest_path ( ) ;
138
fs:: create_dir_all( p.parent ( ) .unwrap ( ) ) ? ;
139
write_atomic ( & p, text.as_bytes ( ) )
140
}
141
b7f102fc Andy McCall 1 month ago ↶
142
143
144
pub ( crate ) fn rewrite_manifest_flags ( & self , conflicted : bool , uncommitted : bool ) -> Result< ( ) > {
145
let p = self .manifest_path ( ) ;
146
let text = match fs:: read_to_string( & p) {
147
Ok ( t) => t,
148
Err ( _ ) => return self .write_manifest ( conflicted, uncommitted) ,
149
} ;
150
let mut out = String :: with_capacity( text.len ( ) ) ;
151
for line in text.lines ( ) {
152
if line.starts_with ( " # conflicted=" ) {
153
out.push_str ( & format! ( " # conflicted={} \n " , conflicted as u8 ) ) ;
154
} else if line.starts_with ( " # uncommitted=" ) {
155
out.push_str ( & format! ( " # uncommitted={} \n " , uncommitted as u8 ) ) ;
156
} else {
157
out.push_str ( line) ;
158
out.push ( ' \n ' ) ;
159
}
160
}
161
write_atomic ( & p, out.as_bytes ( ) )
162
}
163
135212de Andy McCall 1 month ago ↶
164
165
166
167
pub ( crate ) fn drop_manifest ( & self ) {
168
let _ = fs:: remove_file( self .manifest_path ( ) ) ;
169
}
170
b7f102fc Andy McCall 1 month ago ↶
171
pub ( crate ) fn read_manifest ( & self ) -> Result< Option< ( bool , bool , Manifest) > > {
135212de Andy McCall 1 month ago ↶
172
let text = match fs:: read_to_string( self .manifest_path ( ) ) {
173
Ok ( t) => t,
174
Err ( e) if e.kind ( ) = = std:: io:: ErrorKind:: NotFound => return Ok ( None ) ,
175
Err ( e) => return Err ( e.into ( ) ) ,
176
} ;
b7f102fc Andy McCall 1 month ago ↶
177
let mut out = Manifest:: default( ) ;
135212de Andy McCall 1 month ago ↶
178
let mut conflicted = false ;
179
let mut uncommitted = false ;
180
for line in text.lines ( ) {
181
if let Some ( v) = line.strip_prefix ( " # conflicted=" ) {
182
conflicted = v.trim ( ) = = " 1" ;
183
continue ;
184
}
185
if let Some ( v) = line.strip_prefix ( " # uncommitted=" ) {
186
uncommitted = v.trim ( ) = = " 1" ;
187
continue ;
188
}
b7f102fc Andy McCall 1 month ago ↶
189
if let Some ( v) = line.strip_prefix ( " # written=" ) {
190
out.written_ns = v.trim ( ) .parse ( ) .unwrap_or ( u128:: MAX ) ;
191
continue ;
192
}
193
let f: Vec< & str > = line.splitn ( 5 , ' \t ' ) .collect ( ) ;
194
if f.len ( ) = = 5 {
195
out.entries.insert (
196
f[ 4 ] .to_string ( ) ,
197
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 ] ) } ,
198
) ;
135212de Andy McCall 1 month ago ↶
199
}
200
}
201
Ok ( Some ( ( conflicted, uncommitted, out) ) )
202
}
203
204
205
206
pub fn quick_dirty ( & self ) -> Result< Option< bool > > {
207
let Some ( ( _ , uncommitted, manifest) ) = self .read_manifest ( ) ? else { return Ok ( None ) } ;
208
if uncommitted {
209
return Ok ( Some ( true ) ) ;
210
}
211
let mut seen = 0 usize ;
212
let mut wb = ignore:: WalkBuilder:: new( & self .root) ;
213
wb.hidden ( false ) .git_ignore ( false ) .git_global ( false ) .git_exclude ( false ) .require_git ( false ) .follow_links ( false ) .add_custom_ignore_filename ( " .oxyignore" ) ;
214
wb.filter_entry ( | e | e.file_name ( ) ! = OXY_DIR ) ;
215
for entry in wb.build ( ) {
216
let Ok ( entry) = entry else { continue } ;
217
let Some ( ft) = entry.file_type ( ) else { continue } ;
218
if ft.is_dir ( ) {
219
continue ;
220
}
221
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 ↶
222
if ! self .in_scope ( & rel) {
223
continue ;
224
}
b7f102fc Andy McCall 1 month ago ↶
225
let Some ( me) = manifest.entries.get ( & rel) else { return Ok ( Some ( true ) ) } ;
135212de Andy McCall 1 month ago ↶
226
seen + = 1 ;
227
let m = fs:: symlink_metadata( entry.path ( ) ) ? ;
b7f102fc Andy McCall 1 month ago ↶
228
let now_mtime = mtime_ns ( & m) ;
135212de Andy McCall 1 month ago ↶
229
if ft.is_symlink ( ) {
230
let target = fs:: read_link( entry.path ( ) ) ? ;
b7f102fc Andy McCall 1 month ago ↶
231
if blake3:: hash( target.to_string_lossy ( ) .as_bytes ( ) ) .to_hex ( ) .as_str ( ) ! = me.hash {
135212de Andy McCall 1 month ago ↶
232
return Ok ( Some ( true ) ) ;
233
}
234
continue ;
235
}
b7f102fc Andy McCall 1 month ago ↶
236
if m.len ( ) ! = me.size {
135212de Andy McCall 1 month ago ↶
237
return Ok ( Some ( true ) ) ;
238
}
b7f102fc Andy McCall 1 month ago ↶
239
if now_mtime ! = me.mtime_ns & & blake3:: hash( & fs:: read( entry.path ( ) ) ? ) .to_hex ( ) .as_str ( ) ! = me.hash {
135212de Andy McCall 1 month ago ↶
240
return Ok ( Some ( true ) ) ;
241
}
242
}
b7f102fc Andy McCall 1 month ago ↶
243
Ok ( Some ( seen ! = manifest.entries.len ( ) ) )
135212de Andy McCall 1 month ago ↶
244
}
245
246
247
pub fn quick_status ( & self ) -> Result< PromptInfo> {
248
let view = self .view ( ) ? ;
249
let bookmark = self .config.bookmark.clone ( ) ;
250
let heads = self .heads_of ( & view, & bookmark) ? ;
251
let wc_hash = view.workspaces.get ( & self .workspace) .copied ( ) ;
252
let change_id = match wc_hash {
253
Some ( h) => self .store.get_change ( & h) ? .change_id.short ( 8 ) ,
254
None => String :: new( ) ,
255
} ;
256
257
258
let manifest = self .read_manifest ( ) ? ;
259
let conflicted = manifest.as_ref ( ) .map ( | ( c , _, _) | * c) .unwrap_or ( false ) ;
260
let dirty = self .quick_dirty ( ) ? .unwrap_or ( false ) ;
261
let ( ahead, behind) = match view.remotes.get ( " origin" ) .and_then ( | b | b.get ( & bookmark) ) {
262
Some ( st) => {
263
let remote_heads = self .store.get_state ( st) ? .heads;
264
let ahead = self .negotiate ( & remote_heads, & heads) .map ( | v | v.len ( ) ) .ok ( ) ;
265
let behind = self .negotiate ( & heads, & remote_heads) .map ( | v | v.len ( ) ) .ok ( ) ;
266
( ahead, behind)
267
}
268
None => ( None , None ) ,
269
} ;
270
Ok ( PromptInfo { bookmark, change_id, heads: heads.len ( ) , dirty, conflicted, ahead, behind } )
271
}
272
}