98286406 Andy McCall 1 month ago ↶
1
2
3
83e0677b Andy McCall 1 month ago ↶
4
5
6
7
98286406 Andy McCall 1 month ago ↶
8
9
10
use crate :: attest:: { Attest, AttestBody} ;
11
use crate :: diff:: { ranges, split_lines, text_edits} ;
83e0677b Andy McCall 1 month ago ↶
12
use crate :: protocol:: { open_remote, Bundle, RefResult, RefUpdate, RefsResponse, RemoteOptions, RemoteRef, SignedAttest, SignedPolicy} ;
4dee277c Andy McCall 1 month ago ↶
13
# [ allow ( unused_imports ) ]
14
use crate :: subrepo:: Pin;
98286406 Andy McCall 1 month ago ↶
15
use crate :: hash:: { hash_object, ChangeHash, KeyId, ObjectRef, ObjectType} ;
16
use crate :: identity:: { verify as verify_signature, Domain, Identity, SecretKey, Signature} ;
17
use crate :: ids:: { ChangeId, NodeId, WorkspaceId} ;
18
use crate :: objects:: { Blob, Change, ChangeKind, Edit, Hlc, Mode, NodeKind, OpRef, StateObject, TreeOp} ;
19
use crate :: oplog:: { Format1Op, Op, View, SNAPSHOT_SUFFIX } ;
20
use crate :: state:: { FileContent, Snapshot, State} ;
21
use crate :: store:: { Store, StoreError} ;
22
use crate :: tree:: OpKey;
23
use rand:: RngCore;
24
use std:: collections:: { BTreeMap, BTreeSet, HashMap} ;
25
use std:: fs;
26
use std:: io:: Write;
27
use std:: path:: { Path, PathBuf} ;
28
29
pub const OXY_DIR : & str = " .oxy" ;
30
pub const FORMAT_LINE : & str = " oxy repository format 2\n " ;
31
const FORMAT1_LINE : & str = " oxy repository format 1\n " ;
32
const BINARY_SNIFF : usize = 8 * 1024 ;
d02e545d Andy McCall 1 month ago ↶
33
34
pub const AUTOPACK_THRESHOLD : usize = 4096 ;
98286406 Andy McCall 1 month ago ↶
35
36
# [ derive ( Debug, thiserror::Error ) ]
37
pub enum RepoError {
38
# [ error ( " io: {0}" ) ]
39
Io( # [ from ] std:: io:: Error) ,
40
# [ error ( transparent ) ]
41
Store( # [ from ] StoreError) ,
42
# [ error ( transparent ) ]
43
Object( # [ from ] crate :: objects:: ObjectError) ,
44
# [ error ( transparent ) ]
45
State( # [ from ] crate :: state:: StateError) ,
46
# [ error ( " {0}" ) ]
47
Msg( String ) ,
48
}
49
50
pub type Result < T> = std:: result:: Result< T, RepoError> ;
51
52
fn fail < T> ( s : impl Into< String> ) -> Result< T> {
53
Err ( RepoError:: Msg( s.into ( ) ) )
54
}
55
7b9809e6 Andy McCall 1 month ago ↶
56
mod bookmarks ;
98286406 Andy McCall 1 month ago ↶
57
mod checkout ;
4dee277c Andy McCall 1 month ago ↶
58
mod subrepos ;
252e8cb2 Andy McCall 1 month ago ↶
59
mod checkpoint ;
98286406 Andy McCall 1 month ago ↶
60
mod config ;
ba160f60 Andy McCall 1 month ago ↶
61
mod gc ;
79ab8528 Andy McCall 1 month ago ↶
62
mod git ;
614541c3 Andy McCall 1 month ago ↶
63
mod history ;
6d3eefbc Andy McCall 1 month ago ↶
64
mod hooks ;
957fee30 Andy McCall 1 month ago ↶
65
mod inspect ;
98286406 Andy McCall 1 month ago ↶
66
mod identity ;
83e0677b Andy McCall 1 month ago ↶
67
mod policy ;
135212de Andy McCall 1 month ago ↶
68
mod prompt ;
98286406 Andy McCall 1 month ago ↶
69
mod snapshot ;
70
mod sync ;
132b884b Andy McCall 1 month ago ↶
71
mod tags ;
4e88e774 Andy McCall 1 month ago ↶
72
mod workspace ;
98286406 Andy McCall 1 month ago ↶
73
# [ cfg ( test ) ]
74
mod tests ;
75
7b9809e6 Andy McCall 1 month ago ↶
76
pub use bookmarks:: { BookmarkInfo, MergeReport} ;
4dee277c Andy McCall 1 month ago ↶
77
pub use subrepos:: { SubrepoInfo, SubrepoSync} ;
98286406 Andy McCall 1 month ago ↶
78
pub use checkout:: CheckoutReport;
b3c5a59a Andy McCall 1 month ago ↶
79
pub use config:: { parse_size, read_config_file, Config} ;
ba160f60 Andy McCall 1 month ago ↶
80
pub use gc:: GcReport;
6d3eefbc Andy McCall 1 month ago ↶
81
pub use hooks:: { json_array, json_str, HookOutcome} ;
957fee30 Andy McCall 1 month ago ↶
82
pub use inspect:: { BisectStep, BlameLine} ;
694805cc Andy McCall 1 month ago ↶
83
pub use prompt:: { PromptInfo, DEFAULT_PROMPT_FORMAT , GLYPH_PROMPT_FORMAT } ;
c3069537 Andy McCall 1 month ago ↶
84
pub use git:: { GitExportReport, GitImportReport} ;
28fc23d7 Andy McCall 1 month ago ↶
85
pub use identity:: { Rotation, SignatureStatus} ;
86
pub use policy:: PolicyUpdate;
c6aa5bd9 Andy McCall 1 month ago ↶
87
pub use snapshot:: { added_node_paths, diff_files_of, has_conflict_markers, is_binary, referenced_changes, render_change, render_hunks, similarity, ChangeStatus, DiffHunk, FileDiff, Hunk, PathStatus, Selection, SnapshotResult} ;
83e0677b Andy McCall 1 month ago ↶
88
pub use sync:: { FetchReport, PullReport} ;
132b884b Andy McCall 1 month ago ↶
89
pub use tags:: TagInfo;
4e88e774 Andy McCall 1 month ago ↶
90
pub use workspace:: { SnapshotLock, WorkspaceInfo} ;
98286406 Andy McCall 1 month ago ↶
91
92
fn write_atomic ( path : & Path, data : & [u8 ]) -> Result< ( ) > {
93
let tmp = path.with_extension ( " tmp" ) ;
94
{
95
let mut f = fs:: File:: create( & tmp) ? ;
96
f.write_all ( data) ? ;
97
f.sync_all ( ) ? ;
98
}
99
fs:: rename( & tmp, path) ? ;
100
Ok ( ( ) )
101
}
102
103
pub struct LogEntry {
104
pub hash : ChangeHash,
2863456a Andy McCall 1 month ago ↶
105
106
pub change : crate:: state:: ChangeMeta,
98286406 Andy McCall 1 month ago ↶
107
pub is_working_copy : bool ,
108
pub is_head : bool ,
109
}
110
c3069537 Andy McCall 1 month ago ↶
111
112
pub ( crate ) struct FsEntry {
113
pub kind : NodeKind,
114
pub exec : bool ,
b7f102fc Andy McCall 1 month ago ↶
115
c3069537 Andy McCall 1 month ago ↶
116
pub content : Vec< u8 > ,
b7f102fc Andy McCall 1 month ago ↶
117
118
119
120
pub blob : Option< ObjectRef> ,
121
122
123
pub content_hash : Option< String> ,
98286406 Andy McCall 1 month ago ↶
124
}
125
b7f102fc Andy McCall 1 month ago ↶
126
impl FsEntry {
127
pub ( crate ) fn new ( kind : NodeKind, exec : bool , content : Vec< u8 > ) -> FsEntry {
128
FsEntry { kind, exec, content, blob: None , content_hash: None }
129
}
130
}
131
98286406 Andy McCall 1 month ago ↶
132
pub struct Repo {
4dee277c Andy McCall 1 month ago ↶
133
134
pub (crate) git_export_skipped : std:: cell:: Cell< usize > ,
98286406 Andy McCall 1 month ago ↶
135
root : PathBuf,
136
oxy : PathBuf,
137
store : Store,
138
workspace : WorkspaceId,
e02bf0cd Andy McCall 1 month ago ↶
139
140
bare : bool ,
98286406 Andy McCall 1 month ago ↶
141
keys_dir : PathBuf,
142
user_config : PathBuf,
143
pub config : Config,
144
}
145
e02bf0cd Andy McCall 1 month ago ↶
146
147
const BARE_MARKER : & str = " bare" ;
148
98286406 Andy McCall 1 month ago ↶
149
150
151
pub fn default_home ( ) -> PathBuf {
152
if let Ok ( h) = std:: env:: var( " OXY_HOME" ) {
153
return PathBuf:: from( h) ;
154
}
155
if let Ok ( x) = std:: env:: var( " XDG_CONFIG_HOME" ) {
156
return PathBuf:: from( x) .join ( " oxy" ) ;
157
}
158
let home = std:: env:: var( " HOME" ) .unwrap_or_else ( | _| " ." .into ( ) ) ;
159
PathBuf:: from( home) .join ( " .config" ) .join ( " oxy" )
160
}
161
162
pub fn default_keys_dir ( ) -> PathBuf {
163
default_home ( ) .join ( " keys" )
164
}
165
166
pub fn default_user_config ( ) -> PathBuf {
167
default_home ( ) .join ( " config.toml" )
168
}
169
170
171
# [ derive ( Clone, Copy, Debug, PartialEq, Eq ) ]
172
pub enum IdentityScope {
173
174
User,
175
176
Repo,
177
}
178
179
impl Repo {
180
181
182
pub fn init ( root : & Path) -> Result< Repo> {
183
let mut id = [ 0 u8 ; 16 ] ;
184
rand:: rngs:: OsRng.fill_bytes ( & mut id) ;
185
Self :: init_with_id( root, id)
186
}
187
188
189
190
pub fn init_with_id ( root : & Path, id : [u8 ; 16]) -> Result< Repo> {
191
fs:: create_dir_all( root) ? ;
192
let root = root.canonicalize ( ) .unwrap_or_else ( | _| root.to_path_buf ( ) ) ;
193
let oxy = root.join ( OXY_DIR ) ;
194
if oxy.exists ( ) {
195
return fail ( format! ( " {} is already an oxy repository" , root.display ( ) ) ) ;
196
}
197
fs:: create_dir_all( oxy.join ( " ops" ) ) ? ;
198
write_atomic ( & oxy.join ( " format" ) , FORMAT_LINE .as_bytes ( ) ) ? ;
199
write_atomic ( & oxy.join ( " id" ) , hex:: encode( id) .as_bytes ( ) ) ? ;
200
let config = Config:: load( Some ( & default_user_config ( ) ) , & oxy.join ( " config" ) ) ? ;
201
config.save ( & oxy.join ( " config" ) ) ? ;
202
let workspace = WorkspaceId:: random( ) ;
203
write_atomic ( & oxy.join ( " workspace" ) , workspace.to_hex ( ) .as_bytes ( ) ) ? ;
204
let store = Store:: open( oxy.join ( " objects" ) ) ? ;
e02bf0cd Andy McCall 1 month ago ↶
205
let repo = Repo { git_export_skipped: Default :: default( ) , root, oxy, store, workspace, bare: false , keys_dir: default_keys_dir ( ) , user_config: default_user_config ( ) , config } ;
4e88e774 Andy McCall 1 month ago ↶
206
repo.register_workspace ( & workspace, & repo.root) ? ;
98286406 Andy McCall 1 month ago ↶
207
208
let wc = repo.new_working_copy ( vec! [ ] , " " ) ;
209
let wc_hash = repo.store.put_change ( & wc) ? ;
210
let mut view = View:: default( ) ;
211
view.bookmarks.insert ( repo.config.bookmark.clone ( ) , repo.store.put_state ( & StateObject:: new( vec! [ ] ) ) ? ) ;
212
view.workspaces.insert ( workspace, wc_hash) ;
213
repo.record_op ( & [ ] , " oxy init" , view) ? ;
214
Ok ( repo)
215
}
216
e02bf0cd Andy McCall 1 month ago ↶
217
218
219
220
pub fn init_bare ( root : & Path) -> Result< Repo> {
221
let mut id = [ 0 u8 ; 16 ] ;
222
rand:: rngs:: OsRng.fill_bytes ( & mut id) ;
223
Self :: init_bare_with_id( root, id)
224
}
225
226
pub fn init_bare_with_id ( root : & Path, id : [u8 ; 16]) -> Result< Repo> {
227
fs:: create_dir_all( root) ? ;
228
let root = root.canonicalize ( ) .unwrap_or_else ( | _| root.to_path_buf ( ) ) ;
229
let oxy = root.join ( OXY_DIR ) ;
230
if oxy.exists ( ) {
231
return fail ( format! ( " {} is already an oxy repository" , root.display ( ) ) ) ;
232
}
233
fs:: create_dir_all( oxy.join ( " ops" ) ) ? ;
234
write_atomic ( & oxy.join ( " format" ) , FORMAT_LINE .as_bytes ( ) ) ? ;
235
write_atomic ( & oxy.join ( " id" ) , hex:: encode( id) .as_bytes ( ) ) ? ;
236
write_atomic ( & oxy.join ( BARE_MARKER ) , b " " ) ? ;
237
let config = Config:: load( Some ( & default_user_config ( ) ) , & oxy.join ( " config" ) ) ? ;
238
config.save ( & oxy.join ( " config" ) ) ? ;
239
let store = Store:: open( oxy.join ( " objects" ) ) ? ;
240
let repo = Repo { git_export_skipped: Default :: default( ) , root, oxy, store, workspace: WorkspaceId:: ZERO , bare: true , keys_dir: default_keys_dir ( ) , user_config: default_user_config ( ) , config } ;
241
let mut view = View:: default( ) ;
242
view.bookmarks.insert ( repo.config.bookmark.clone ( ) , repo.store.put_state ( & StateObject:: new( vec! [ ] ) ) ? ) ;
243
repo.record_op ( & [ ] , " oxy init --bare" , view) ? ;
244
Ok ( repo)
245
}
246
247
248
pub fn is_bare ( & self ) -> bool {
249
self .bare
250
}
251
98286406 Andy McCall 1 month ago ↶
252
253
pub fn open ( start : & Path) -> Result< Repo> {
254
let start = start.canonicalize ( ) ? ;
255
let mut dir: Option< & Path> = Some ( & start) ;
256
while let Some ( d) = dir {
257
let oxy = d.join ( OXY_DIR ) ;
4e88e774 Andy McCall 1 month ago ↶
258
if oxy.is_file ( ) {
259
260
let text = fs:: read_to_string( & oxy) ? ;
261
let Some ( ( shared, workspace) ) = workspace:: parse_workspace_file( & text) else {
262
return fail ( format! ( " {} is not a valid workspace file" , oxy.display ( ) ) ) ;
263
} ;
264
let format = fs:: read_to_string( shared.join ( " format" ) ) ? ;
265
if format ! = FORMAT_LINE {
266
return fail ( format! ( " unsupported repository format: {} " , format.trim ( ) ) ) ;
267
}
268
let config = Config:: load( Some ( & default_user_config ( ) ) , & shared.join ( " config" ) ) ? ;
269
let store = Store:: open( shared.join ( " objects" ) ) ? ;
e02bf0cd Andy McCall 1 month ago ↶
270
let mut repo = Repo { git_export_skipped: Default :: default( ) , root: d.to_path_buf ( ) , oxy: shared, store, workspace, bare: false , keys_dir: default_keys_dir ( ) , user_config: default_user_config ( ) , config } ;
7b9809e6 Andy McCall 1 month ago ↶
271
repo.apply_workspace_bookmark ( ) ;
272
return Ok ( repo) ;
4e88e774 Andy McCall 1 month ago ↶
273
}
98286406 Andy McCall 1 month ago ↶
274
if oxy.is_dir ( ) {
275
let format = fs:: read_to_string( oxy.join ( " format" ) ) ? ;
276
let config = Config:: load( Some ( & default_user_config ( ) ) , & oxy.join ( " config" ) ) ? ;
e02bf0cd Andy McCall 1 month ago ↶
277
let bare = oxy.join ( BARE_MARKER ) .is_file ( ) ;
278
let workspace = if bare {
279
WorkspaceId:: ZERO
280
} else {
281
WorkspaceId:: from_hex( fs:: read_to_string( oxy.join ( " workspace" ) ) ? .trim ( ) ) .ok_or_else ( | | RepoError:: Msg( " corrupt .oxy/workspace" .into ( ) ) ) ?
282
} ;
98286406 Andy McCall 1 month ago ↶
283
let store = Store:: open( oxy.join ( " objects" ) ) ? ;
e02bf0cd Andy McCall 1 month ago ↶
284
let mut repo = Repo { git_export_skipped: Default :: default( ) , root: d.to_path_buf ( ) , oxy, store, workspace, bare, keys_dir: default_keys_dir ( ) , user_config: default_user_config ( ) , config } ;
285
if ! bare {
286
repo.apply_workspace_bookmark ( ) ;
287
}
98286406 Andy McCall 1 month ago ↶
288
if format = = FORMAT1_LINE {
289
repo.migrate_format1 ( ) ? ;
290
} else if format ! = FORMAT_LINE {
291
return fail ( format! ( " unsupported repository format: {} " , format.trim ( ) ) ) ;
292
}
293
return Ok ( repo) ;
294
}
295
dir = d.parent ( ) ;
296
}
297
fail ( format! ( " no oxy repository found at or above {} " , start.display ( ) ) )
298
}
299
300
301
302
fn migrate_format1 ( & self ) -> Result< ( ) > {
303
let hex = fs:: read_to_string( self .oxy.join ( " ops" ) .join ( " head" ) ) ? ;
304
let head = ObjectRef:: from_hex( hex.trim ( ) ) .ok_or_else ( | | RepoError:: Msg( " corrupt .oxy/ops/head" .into ( ) ) ) ? ;
305
306
let mut chain: Vec< Format1Op> = Vec :: new( ) ;
307
let mut cur = Some ( head) ;
308
while let Some ( r) = cur {
309
let op = Op:: decode_format1( & self .store.get_typed ( & r, ObjectType:: Op) ? ) ? ;
310
cur = op.parents.first ( ) .copied ( ) ;
311
chain.push ( op) ;
312
}
313
chain.reverse ( ) ;
314
let mut new_parent: Option< ObjectRef> = None ;
315
for old in chain {
316
let mut view = View:: default( ) ;
317
for ( name, heads) in & old.view.bookmarks {
318
view.bookmarks.insert ( name.clone ( ) , self .store.put_state ( & StateObject:: new( heads.clone ( ) ) ) ? ) ;
319
}
320
view.workspaces = old.view.workspaces.clone ( ) ;
321
let op = Op { parents: new_parent.into_iter ( ) .collect ( ) , hlc: old.hlc, command: old.command, view } ;
322
new_parent = Some ( self .store.put ( ObjectType:: Op, & op.encode ( ) ) ? ) ;
323
}
324
write_atomic ( & self .oxy.join ( " ops" ) .join ( " head" ) , new_parent.unwrap ( ) .to_hex ( ) .as_bytes ( ) ) ? ;
325
write_atomic ( & self .oxy.join ( " format" ) , FORMAT_LINE .as_bytes ( ) ) ? ;
326
Ok ( ( ) )
d02e545d Andy McCall 1 month ago ↶
327
}
328
329
330
pub ( crate ) fn maybe_autopack ( & self ) -> Result< ( ) > {
331
if self .store.loose_count ( ) ? > AUTOPACK_THRESHOLD {
332
self .store.pack_loose ( ) ? ;
135212de Andy McCall 1 month ago ↶
333
self .store.merge_packs ( ) ? ;
d02e545d Andy McCall 1 month ago ↶
334
}
335
Ok ( ( ) )
336
}
337
98286406 Andy McCall 1 month ago ↶
338
339
pub fn id ( & self ) -> Result< [ u8 ; 16 ] > {
340
let path = self .oxy.join ( " id" ) ;
341
match fs:: read_to_string( & path) {
342
Ok ( h) => hex:: decode( h.trim ( ) ) .ok ( ) .and_then ( | v | v.try_into ( ) .ok ( ) ) .ok_or_else ( | | RepoError:: Msg( " corrupt .oxy/id" .into ( ) ) ) ,
343
Err ( e) if e.kind ( ) = = std:: io:: ErrorKind:: NotFound => {
344
let mut id = [ 0 u8 ; 16 ] ;
345
rand:: rngs:: OsRng.fill_bytes ( & mut id) ;
346
write_atomic ( & path, hex:: encode( id) .as_bytes ( ) ) ? ;
347
Ok ( id)
348
}
349
Err ( e) => Err ( e.into ( ) ) ,
350
}
351
}
352
353
pub fn root ( & self ) -> & Path {
354
& self .root
355
}
356
pub fn store ( & self ) -> & Store {
357
& self .store
358
}
359
pub fn workspace ( & self ) -> WorkspaceId {
360
self .workspace
361
}
362
363
364
365
pub fn with_home ( mut self , dir : impl Into< PathBuf> ) -> Repo {
366
let dir = dir.into ( ) ;
367
self .keys_dir = dir.join ( " keys" ) ;
368
self .user_config = dir.join ( " config.toml" ) ;
369
if let Ok ( c) = Config:: load( Some ( & self .user_config) , & self .oxy.join ( " config" ) ) {
370
self .config = c;
371
}
7b9809e6 Andy McCall 1 month ago ↶
372
self .apply_workspace_bookmark ( ) ;
98286406 Andy McCall 1 month ago ↶
373
self
374
}
375
7b9809e6 Andy McCall 1 month ago ↶
376
377
378
fn apply_workspace_bookmark ( & mut self ) {
379
if let Ok ( b) = fs:: read_to_string( self .oxy.join ( " workspaces" ) .join ( self .workspace.to_hex ( ) ) .join ( " bookmark" ) ) {
380
let b = b.trim ( ) ;
381
if ! b.is_empty ( ) {
382
self .config.bookmark = b.to_string ( ) ;
383
}
384
}
385
}
386
98286406 Andy McCall 1 month ago ↶
387
pub fn user_config_path ( & self ) -> & Path {
388
& self .user_config
389
}
390
391
392
pub fn set_user_config ( & mut self , key : & str , value : & str ) -> Result< ( ) > {
393
Config:: set_user( & self .user_config, key, value) ? ;
394
self .config = Config:: load( Some ( & self .user_config) , & self .oxy.join ( " config" ) ) ? ;
395
Ok ( ( ) )
396
}
397
398
pub fn save_config ( & self ) -> Result< ( ) > {
399
self .config.save ( & self .oxy.join ( " config" ) )
400
}
401
402
fn author ( & self ) -> KeyId {
403
self .config.author_key.unwrap_or ( ObjectRef:: ZERO )
404
}
405
}
406
407
impl Repo {
408
fn new_working_copy ( & self , deps : Vec< ChangeHash> , message : & str ) -> Change {
409
let mut c = Change {
410
change_id: ChangeId:: random( ) ,
411
deps,
412
author: self .author ( ) ,
413
hlc: Hlc:: now( ) ,
414
message: message.into ( ) ,
415
tree_ops: vec! [ ] ,
416
edits: vec! [ ] ,
417
kind: ChangeKind:: WorkingCopy,
418
} ;
419
c.normalize ( ) ;
420
c
421
}
422
423
424
425
pub fn head_op ( & self ) -> Result< ( ObjectRef, Op) > {
426
let hex = fs:: read_to_string( self .oxy.join ( " ops" ) .join ( " head" ) ) ? ;
427
let r = ObjectRef:: from_hex( hex.trim ( ) ) .ok_or_else ( | | RepoError:: Msg( " corrupt .oxy/ops/head" .into ( ) ) ) ? ;
428
let op = Op:: decode( & self .store.get_typed ( & r, ObjectType:: Op) ? ) ? ;
429
Ok ( ( r, op) )
430
}
431
432
pub fn view ( & self ) -> Result< View> {
433
Ok ( self .head_op ( ) ? .1. view)
434
}
435
436
fn record_op ( & self , parents : & [ObjectRef], command : & str , view : View) -> Result< ObjectRef> {
437
let op = Op { parents: parents.to_vec ( ) , hlc: Hlc:: now( ) , command: command.into ( ) , view } ;
438
let r = self .store.put ( ObjectType:: Op, & op.encode ( ) ) ? ;
439
write_atomic ( & self .oxy.join ( " ops" ) .join ( " head" ) , r.to_hex ( ) .as_bytes ( ) ) ? ;
440
Ok ( r)
441
}
442
443
pub fn op_log ( & self ) -> Result< Vec< ( ObjectRef, Op) > > {
444
let mut out = Vec :: new( ) ;
445
let ( mut r, mut op) = self .head_op ( ) ? ;
446
loop {
447
let next = op.parents.first ( ) .copied ( ) ;
448
out.push ( ( r, op) ) ;
449
match next {
450
Some ( p) => {
451
r = p;
452
op = Op:: decode( & self .store.get_typed ( & r, ObjectType:: Op) ? ) ? ;
453
}
454
None => break ,
455
}
456
}
457
Ok ( out)
458
}
459
460
461
462
463
pub fn undo ( & self ) -> Result< Op> {
464
let ( _ , mut op) = self .head_op ( ) ? ;
465
while op.is_snapshot ( ) {
466
let Some ( parent) = op.parents.first ( ) else { break } ;
467
op = Op:: decode( & self .store.get_typed ( parent, ObjectType:: Op) ? ) ? ;
468
}
469
let Some ( parent) = op.parents.first ( ) else {
470
return fail ( " nothing to undo" ) ;
471
} ;
472
let parent_op = Op:: decode( & self .store.get_typed ( parent, ObjectType:: Op) ? ) ? ;
473
write_atomic ( & self .oxy.join ( " ops" ) .join ( " head" ) , parent.to_hex ( ) .as_bytes ( ) ) ? ;
135212de Andy McCall 1 month ago ↶
474
self .drop_manifest ( ) ;
98286406 Andy McCall 1 month ago ↶
475
Ok ( parent_op)
476
}
477
478
479
pub fn last_command ( & self ) -> Result< Op> {
480
let ( _ , mut op) = self .head_op ( ) ? ;
481
while op.is_snapshot ( ) {
482
let Some ( parent) = op.parents.first ( ) else { break } ;
483
op = Op:: decode( & self .store.get_typed ( parent, ObjectType:: Op) ? ) ? ;
484
}
485
Ok ( op)
486
}
487
488
489
252e8cb2 Andy McCall 1 month ago ↶
490
98286406 Andy McCall 1 month ago ↶
491
pub fn load_state ( & self , heads : & [ChangeHash]) -> Result< State> {
252e8cb2 Andy McCall 1 month ago ↶
492
if let Some ( st) = self .load_state_checkpointed ( heads) ? {
493
return Ok ( st) ;
494
}
495
self .load_state_full ( heads)
496
}
497
498
499
pub fn load_state_full ( & self , heads : & [ChangeHash]) -> Result< State> {
98286406 Andy McCall 1 month ago ↶
500
let mut state = State:: new( ) ;
501
let mut stack: Vec< ChangeHash> = heads.to_vec ( ) ;
502
let mut seen: BTreeSet< ChangeHash> = BTreeSet:: new( ) ;
503
while let Some ( h) = stack.pop ( ) {
504
if ! seen.insert ( h) {
505
continue ;
506
}
507
let c = self .store.get_change ( & h) ? ;
508
stack.extend ( c.deps.iter ( ) .copied ( ) ) ;
509
state.add_unchecked ( c) ;
510
}
511
state.validate ( ) ? ;
512
Ok ( state)
513
}
514
515
pub fn working_copy ( & self ) -> Result< ( ChangeHash, Change) > {
e02bf0cd Andy McCall 1 month ago ↶
516
if self .bare {
517
return fail ( " this is a bare repository: it has no working copy" ) ;
518
}
98286406 Andy McCall 1 month ago ↶
519
let view = self .view ( ) ? ;
520
let h = * view
521
.workspaces
522
.get ( & self .workspace)
523
.ok_or_else ( | | RepoError:: Msg( " this workspace is not in the current view" .into ( ) ) ) ? ;
524
Ok ( ( h, self .store.get_change ( & h) ? ) )
525
}
526
527
528
pub fn heads_of ( & self , view : & View, bookmark : & str ) -> Result< Vec< ChangeHash> > {
529
match view.bookmarks.get ( bookmark) {
530
Some ( st) => Ok ( self .store.get_state ( st) ? .heads) ,
531
None => Ok ( vec! [ ] ) ,
532
}
533
}
534
535
pub fn bookmark_heads ( & self ) -> Result< Vec< ChangeHash> > {
536
self .heads_of ( & self .view ( ) ? , & self .config.bookmark)
537
}
538
539
540
541
fn blob_for ( content : & [u8 ]) -> Blob {
542
Blob {
543
length: content.len ( ) as u64 ,
544
chunks: crate :: chunk:: chunks( content) .into_iter ( ) .map ( | c | hash_object ( ObjectType:: Chunk, c) ) .collect ( ) ,
545
}
546
}
547
548
pub fn blob_ref ( content : & [u8 ]) -> ObjectRef {
549
Self :: blob_for( content) .hash ( )
550
}
551
552
pub fn store_blob ( & self , content : & [u8 ]) -> Result< ObjectRef> {
553
for c in crate :: chunk:: chunks( content) {
554
self .store.put_chunk ( c) ? ;
555
}
556
Ok ( self .store.put ( ObjectType:: Blob, & Self :: blob_for( content) .encode ( ) ) ? )
557
}
558
b3c5a59a Andy McCall 1 month ago ↶
559
560
98286406 Andy McCall 1 month ago ↶
561
pub fn read_blob ( & self , r : & ObjectRef) -> Result< Vec< u8 > > {
b3c5a59a Andy McCall 1 month ago ↶
562
if ! self .store.has ( r) & & self .lazy_remote ( r) .is_some ( ) {
563
self .fetch_lazy_blob ( r) ? ;
564
}
98286406 Andy McCall 1 month ago ↶
565
let blob = Blob:: decode( & self .store.get_typed ( r, ObjectType:: Blob) ? ) ? ;
566
let mut out = Vec :: with_capacity( blob.length as usize ) ;
567
for c in & blob.chunks {
568
out.extend ( self .store.get_typed ( c, ObjectType:: Chunk) ? ) ;
569
}
570
Ok ( out)
571
}
572
573
574
cdb5beba Andy McCall 1 month ago ↶
575
576
pub fn disk_attributes ( & self ) -> crate:: attributes:: Attributes {
577
let mut files: Vec< ( String, Vec< u8 > ) > = Vec :: new( ) ;
578
if let Ok ( b) = fs:: read( self .root.join ( " .oxyattributes" ) ) {
579
files.push ( ( " .oxyattributes" .into ( ) , b) ) ;
580
}
581
let mut wb = ignore:: WalkBuilder:: new( & self .root) ;
582
wb.hidden ( false ) .git_ignore ( false ) .git_global ( false ) .git_exclude ( false ) .require_git ( false ) .follow_links ( false ) .add_custom_ignore_filename ( " .oxyignore" ) ;
583
wb.filter_entry ( | e | e.file_name ( ) ! = OXY_DIR ) ;
584
for entry in wb.build ( ) .flatten ( ) {
585
if entry.file_name ( ) = = " .oxyattributes" & & entry.path ( ) ! = self .root.join ( " .oxyattributes" ) {
586
if let Ok ( b) = fs:: read( entry.path ( ) ) {
587
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 ( " /" ) ;
588
files.push ( ( rel, b) ) ;
589
}
590
}
591
}
592
crate :: attributes:: Attributes:: from_files( files.iter ( ) .map ( | ( p , c ) | ( p.as_str ( ) , c.as_slice ( ) ) ) )
593
}
594
595
596
pub fn state_attributes ( snapshot : & Snapshot) -> crate:: attributes:: Attributes {
597
crate :: attributes:: Attributes:: from_files( snapshot.files.iter ( ) .filter_map ( | ( p , f ) | match & f.content {
598
FileContent:: Text { bytes, .. } if p = = " .oxyattributes" | | p.ends_with ( " /.oxyattributes" ) => Some ( ( p.as_str ( ) , bytes.as_slice ( ) ) ) ,
599
_ => None ,
600
} ) )
601
}
602
603
604
pub fn store_blob_for ( & self , content : & [u8 ], nocompress : bool ) -> Result< ObjectRef> {
605
if ! nocompress {
606
return self .store_blob ( content) ;
607
}
608
for c in crate :: chunk:: chunks( content) {
609
self .store.put_chunk_nocompress ( c) ? ;
610
}
611
Ok ( self .store.put ( ObjectType:: Blob, & Self :: blob_for( content) .encode ( ) ) ? )
612
}
613
614
fn locks_dir ( & self ) -> PathBuf {
615
self .oxy.join ( " locks" )
616
}
617
618
619
pub fn is_locked ( & self , path : & str ) -> bool {
620
self .locks_dir ( ) .join ( path.replace ( ' /' , " %2F" ) ) .exists ( )
621
}
622
623
624
pub fn lock_path ( & self , path : & str ) -> Result< ( ) > {
625
let attrs = self .disk_attributes ( ) ;
626
if ! attrs.lookup ( path) .lockable {
627
return fail ( format! ( " {path} is not marked lockable in .oxyattributes" ) ) ;
628
}
629
fs:: create_dir_all( self .locks_dir ( ) ) ? ;
630
write_atomic ( & self .locks_dir ( ) .join ( path.replace ( ' /' , " %2F" ) ) , std:: process:: id( ) .to_string ( ) .as_bytes ( ) ) ? ;
631
set_readonly ( & self .root.join ( path) , false ) ? ;
632
Ok ( ( ) )
633
}
634
635
636
pub fn unlock_path ( & self , path : & str ) -> Result< ( ) > {
637
let _ = fs:: remove_file( self .locks_dir ( ) .join ( path.replace ( ' /' , " %2F" ) ) ) ;
638
if self .root.join ( path) .exists ( ) {
639
set_readonly ( & self .root.join ( path) , true ) ? ;
640
}
641
Ok ( ( ) )
642
}
643
7421309f Andy McCall 1 month ago ↶
644
645
pub fn in_scope ( & self , path : & str ) -> bool {
646
self .config.sparse_paths.is_empty ( ) | | self .config.sparse_paths.iter ( ) .any ( | p | path = = p | | path.starts_with ( & format! ( " {p} /" ) ) )
647
}
648
98286406 Andy McCall 1 month ago ↶
649
fn walk_working_copy ( & self ) -> Result< BTreeMap< String, FsEntry> > {
4dee277c Andy McCall 1 month ago ↶
650
self .walk_working_copy_with ( None , & BTreeMap:: new( ) )
651
}
652
653
654
655
656
pub ( crate ) fn walker ( & self , nested : & std:: sync:: Arc< std:: sync:: Mutex< Vec< PathBuf> > > ) -> ignore:: WalkBuilder {
657
let mut wb = ignore:: WalkBuilder:: new( & self .root) ;
658
wb.hidden ( false )
659
.git_ignore ( false )
660
.git_global ( false )
661
.git_exclude ( false )
662
.require_git ( false )
663
.follow_links ( false )
664
.add_custom_ignore_filename ( " .oxyignore" ) ;
665
let nested = nested.clone ( ) ;
666
wb.filter_entry ( move | e| {
667
if e.file_name ( ) = = OXY_DIR {
668
return false ;
669
}
670
if e.depth ( ) > 0 & & e.file_type ( ) .is_some_and ( | t | t.is_dir ( ) ) & & ( e.path ( ) .join ( OXY_DIR ) .exists ( ) | | e.path ( ) .join ( " .git" ) .exists ( ) ) {
671
nested.lock ( ) .unwrap ( ) .push ( e.path ( ) .to_path_buf ( ) ) ;
672
return false ;
673
}
674
true
675
} ) ;
676
wb
b7f102fc Andy McCall 1 month ago ↶
677
}
678
679
680
681
682
9ac1fbe2 Andy McCall 1 month ago ↶
683
684
685
686
4dee277c Andy McCall 1 month ago ↶
687
fn walk_working_copy_with ( & self , cache : Option< & prompt:: Manifest> , tracked : & BTreeMap< String, ( NodeKind, Option< ObjectRef> ) > ) -> Result< BTreeMap< String, FsEntry> > {
688
let nested = std:: sync:: Arc:: new( std:: sync:: Mutex:: new( Vec :: new( ) ) ) ;
689
let wb = self .walker ( & nested) ;
98286406 Andy McCall 1 month ago ↶
690
let mut out = BTreeMap:: new( ) ;
691
for entry in wb.build ( ) {
692
let entry = entry.map_err ( | e | RepoError:: Msg( e.to_string ( ) ) ) ? ;
693
let path = entry.path ( ) ;
694
if path = = self .root {
695
continue ;
696
}
697
let rel = path.strip_prefix ( & self .root) .unwrap ( ) ;
698
let rel: String = rel.components ( ) .map ( | c | c.as_os_str ( ) .to_string_lossy ( ) .to_string ( ) ) .collect:: < Vec< _ > > ( ) .join ( " /" ) ;
699
let Some ( ft) = entry.file_type ( ) else { continue } ;
7421309f Andy McCall 1 month ago ↶
700
if ft.is_dir ( ) | | ! self .in_scope ( & rel) {
98286406 Andy McCall 1 month ago ↶
701
continue ;
702
}
703
if ft.is_symlink ( ) {
704
let target = fs:: read_link( path) ? ;
b7f102fc Andy McCall 1 month ago ↶
705
out.insert ( rel, FsEntry:: new( NodeKind:: Symlink, false , target.to_string_lossy ( ) .as_bytes ( ) .to_vec ( ) ) ) ;
98286406 Andy McCall 1 month ago ↶
706
} else if ft.is_file ( ) {
b7f102fc Andy McCall 1 month ago ↶
707
let meta = entry.metadata ( ) .map_err ( | e | RepoError:: Msg( e.to_string ( ) ) ) ? ;
9ac1fbe2 Andy McCall 1 month ago ↶
708
let e = self .file_entry ( & rel, path, & meta, cache) ? ;
709
out.insert ( rel, e) ;
98286406 Andy McCall 1 month ago ↶
710
}
711
}
4dee277c Andy McCall 1 month ago ↶
712
713
714
715
let modules = self .modules_on_disk ( ) ;
716
let removed = self .removed_subrepos ( ) ;
717
for rel in & removed {
718
if tracked.get ( rel) .map ( | ( k , _) | * k) ! = Some ( NodeKind:: Subrepo) {
719
self .clear_removed_subrepo ( rel) ;
720
}
721
}
722
let nested_dirs = std:: mem:: take( & mut * nested.lock ( ) .unwrap ( ) ) ;
723
for dir in nested_dirs {
724
let rel = self .rel_path ( & dir) ;
725
if ! self .in_scope ( & rel) | | removed.contains ( & rel) {
726
continue ;
727
}
728
let registered = modules.entries.get ( & rel) ;
729
if registered.is_none ( ) & & tracked.get ( & rel) .map ( | ( k , _) | * k) ! = Some ( NodeKind:: Subrepo) {
730
continue ;
731
}
732
let pin = subrepos:: read_pin( & dir, registered.and_then ( | m | m.bookmark.as_deref ( ) ) ) ? ;
733
out.insert ( rel, FsEntry:: new( NodeKind:: Subrepo, false , pin.encode ( ) ) ) ;
734
}
9ac1fbe2 Andy McCall 1 month ago ↶
735
4dee277c Andy McCall 1 month ago ↶
736
for ( rel, ( kind, blob) ) in tracked {
9ac1fbe2 Andy McCall 1 month ago ↶
737
if out.contains_key ( rel) | | ! self .in_scope ( rel) {
738
continue ;
739
}
740
let path = self .root.join ( rel) ;
4dee277c Andy McCall 1 month ago ↶
741
if * kind = = NodeKind:: Subrepo {
742
if removed.contains ( rel) {
743
continue ;
744
}
745
let present = path.is_dir ( ) & & ( path.join ( OXY_DIR ) .exists ( ) | | path.join ( " .git" ) .exists ( ) ) ;
746
if present {
747
let pin = subrepos:: read_pin( & path, modules.entries.get ( rel) .and_then ( | m | m.bookmark.as_deref ( ) ) ) ? ;
748
out.insert ( rel.clone ( ) , FsEntry:: new( NodeKind:: Subrepo, false , pin.encode ( ) ) ) ;
749
} else if let Some ( b) = blob {
750
751
752
out.insert ( rel.clone ( ) , FsEntry { kind: NodeKind:: Subrepo, exec: false , content: vec! [ ] , blob: Some ( * b) , content_hash: None } ) ;
753
}
754
continue ;
755
}
9ac1fbe2 Andy McCall 1 month ago ↶
756
let Ok ( meta) = fs:: symlink_metadata( & path) else { continue } ;
757
let ft = meta.file_type ( ) ;
758
if ft.is_symlink ( ) {
759
let target = fs:: read_link( & path) ? ;
760
out.insert ( rel.clone ( ) , FsEntry:: new( NodeKind:: Symlink, false , target.to_string_lossy ( ) .as_bytes ( ) .to_vec ( ) ) ) ;
761
} else if ft.is_file ( ) {
762
let e = self .file_entry ( rel, & path, & meta, cache) ? ;
763
out.insert ( rel.clone ( ) , e) ;
764
}
765
}
98286406 Andy McCall 1 month ago ↶
766
Ok ( out)
767
}
9ac1fbe2 Andy McCall 1 month ago ↶
768
769
770
fn file_entry ( & self , rel : & str , path : & Path, meta : & fs:: Metadata, cache : Option< & prompt:: Manifest> ) -> Result< FsEntry> {
771
let exec = is_executable ( meta) ;
772
let cached = cache.and_then ( | m | {
773
let e = m.entries.get ( rel) ? ;
774
let mtime_ns = prompt:: mtime_ns( meta) ;
775
let racy = mtime_ns + 2_000_000_000 > = m.written_ns;
776
( e.size = = meta.len ( ) & & e.mtime_ns = = mtime_ns & & ! racy) .then_some ( e)
777
} ) ;
778
Ok ( match cached {
779
Some ( e) if e.blob.is_some_and ( | b | self .store.has ( & b) ) => FsEntry { kind: NodeKind:: File, exec, content: vec! [ ] , blob: e.blob, content_hash: Some ( e.hash.clone ( ) ) } ,
780
Some ( e) => FsEntry { kind: NodeKind:: File, exec, content: fs:: read( path) ? , blob: None , content_hash: Some ( e.hash.clone ( ) ) } ,
781
None => FsEntry:: new( NodeKind:: File, exec, fs:: read( path) ? ) ,
782
} )
783
}
784
4dee277c Andy McCall 1 month ago ↶
785
786
787
pub ( crate ) fn tracked_kinds ( snapshot : & Snapshot) -> BTreeMap< String, ( NodeKind, Option< ObjectRef> ) > {
788
snapshot
789
.files
790
.iter ( )
791
.filter ( | ( _, f ) | f.kind ! = NodeKind:: Dir )
792
.map ( | ( p , f ) | {
793
let blob = match & f.content {
794
FileContent:: Opaque { blobs } => blobs.first ( ) .copied ( ) ,
795
_ => None ,
796
} ;
797
( p.clone ( ) , ( f.kind, blob) )
798
} )
799
.collect ( )
800
}
801
802
803
pub ( crate ) fn rel_path ( & self , path : & Path) -> String {
804
path.strip_prefix ( & self .root) .unwrap_or ( path) .components ( ) .map ( | c | c.as_os_str ( ) .to_string_lossy ( ) .to_string ( ) ) .collect:: < Vec< _ > > ( ) .join ( " /" )
9ac1fbe2 Andy McCall 1 month ago ↶
805
}
98286406 Andy McCall 1 month ago ↶
806
}
807
808
impl Repo {
809
810
pub fn log ( & self ) -> Result< Vec< LogEntry> > {
e02bf0cd Andy McCall 1 month ago ↶
811
let heads = self .bookmark_heads ( ) ? ;
812
if self .bare {
813
return self .log_state ( & heads) ;
814
}
98286406 Andy McCall 1 month ago ↶
815
let ( wc_hash, _ ) = self .working_copy ( ) ? ;
e02bf0cd Andy McCall 1 month ago ↶
816
let mut all = heads.clone ( ) ;
817
all.push ( wc_hash) ;
818
let state = self .load_state ( & all) ? ;
819
let head_set: BTreeSet< ChangeHash> = heads.into_iter ( ) .collect ( ) ;
252e8cb2 Andy McCall 1 month ago ↶
820
let mut entries: Vec< LogEntry> = Vec :: new( ) ;
2863456a Andy McCall 1 month ago ↶
821
for ( h, meta) in state.changes ( ) {
822
entries.push ( LogEntry { hash: * h, change: meta.clone ( ) , is_working_copy: * h = = wc_hash, is_head: head_set.contains ( h) } ) ;
252e8cb2 Andy McCall 1 month ago ↶
823
}
98286406 Andy McCall 1 month ago ↶
824
entries.sort_by ( | a , b | b.is_working_copy.cmp ( & a.is_working_copy) .then ( b.change.hlc.cmp ( & a.change.hlc) ) .then ( b.hash.cmp ( & a.hash) ) ) ;
825
Ok ( entries)
826
}
827
e02bf0cd Andy McCall 1 month ago ↶
828
829
830
pub fn log_state ( & self , heads : & [ChangeHash]) -> Result< Vec< LogEntry> > {
831
let state = self .load_state ( heads) ? ;
832
let head_set: BTreeSet< ChangeHash> = state.heads ( ) .into_iter ( ) .collect ( ) ;
833
let mut entries: Vec< LogEntry> = state
834
.changes ( )
835
.map ( | ( h , meta ) | LogEntry { hash: * h, change: meta.clone ( ) , is_working_copy: false , is_head: head_set.contains ( h) } )
836
.collect ( ) ;
837
entries.sort_by ( | a , b | b.change.hlc.cmp ( & a.change.hlc) .then ( b.hash.cmp ( & a.hash) ) ) ;
838
Ok ( entries)
839
}
840
132b884b Andy McCall 1 month ago ↶
841
842
843
pub fn resolve_heads ( & self , spec : & str ) -> Result< Vec< ChangeHash> > {
7b9809e6 Andy McCall 1 month ago ↶
844
let view = self .view ( ) ? ;
845
if let Some ( st) = view.bookmarks.get ( spec) {
846
return Ok ( self .store.get_state ( st) ? .heads) ;
132b884b Andy McCall 1 month ago ↶
847
}
848
if let Some ( heads) = self .tag_heads ( spec) ? {
849
return Ok ( heads) ;
850
}
851
Ok ( vec! [ self .resolve_change ( spec) ? ] )
852
}
853
98286406 Andy McCall 1 month ago ↶
854
855
pub fn resolve_change ( & self , spec : & str ) -> Result< ChangeHash> {
856
if spec = = " @" {
857
return Ok ( self .working_copy ( ) ? .0 ) ;
858
}
7b9809e6 Andy McCall 1 month ago ↶
859
if let Some ( st) = self .view ( ) ? .bookmarks.get ( spec) {
860
let heads = self .store.get_state ( st) ? .heads;
861
return match heads.len ( ) {
862
1 => Ok ( heads[ 0 ] ) ,
863
n => fail ( format! ( " bookmark {spec} has {n} heads; name a change, or use it where a state is expected" ) ) ,
864
} ;
865
}
98286406 Andy McCall 1 month ago ↶
866
if spec.is_empty ( ) | | spec = = " #" {
867
return fail ( " empty change specifier" ) ;
868
}
869
let entries = self .log ( ) ? ;
870
let matches: Vec< & LogEntry> = if let Some ( hp) = spec.strip_prefix ( ' #' ) {
871
entries.iter ( ) .filter ( | e | e.hash.starts_with_hex ( hp) ) .collect ( )
872
} else {
873
entries.iter ( ) .filter ( | e | e.change.change_id.starts_with_hex ( spec) ) .collect ( )
874
} ;
875
match matches.len ( ) {
876
1 => Ok ( matches[ 0 ] .hash) ,
877
0 => fail ( format! ( " no change matches {spec:?} " ) ) ,
878
_ => {
879
let ids: BTreeSet< ChangeId> = matches.iter ( ) .map ( | e | e.change.change_id ) .collect ( ) ;
880
if ids.len ( ) = = 1 {
881
fail ( format! ( " change {spec} is divergent: {} variants" , matches.len ( ) ) )
882
} else {
883
fail ( format! ( " {spec:?} is ambiguous" ) )
884
}
885
}
886
}
887
}
888
889
890
pub fn describe_change ( & self , hash : & ChangeHash) -> Result< String> {
891
let change = self .store.get_change ( hash) ? ;
892
let base = self .load_state ( & change.deps) ? ;
893
let base_snapshot = base.materialize ( ) ;
894
Ok ( render_change ( & change, & base, & base_snapshot) )
895
}
896
}
897
898
fn remove_any ( path : & Path) -> std:: io:: Result< ( ) > {
899
let m = fs:: symlink_metadata( path) ? ;
900
if m.is_dir ( ) { fs:: remove_dir_all( path) } else { fs:: remove_file( path) }
901
}
902
cdb5beba Andy McCall 1 month ago ↶
903
fn set_readonly ( path : & Path, readonly : bool ) -> std:: io:: Result< ( ) > {
904
let mut perms = fs:: metadata( path) ? .permissions ( ) ;
905
perms.set_readonly ( readonly) ;
906
fs:: set_permissions( path, perms)
907
}
908
98286406 Andy McCall 1 month ago ↶
909
# [ cfg ( unix ) ]
910
fn set_executable ( path : & Path, exec : bool ) -> std:: io:: Result< ( ) > {
911
use std:: os:: unix:: fs:: PermissionsExt;
912
let mut perms = fs:: metadata( path) ? .permissions ( ) ;
913
let mode = perms.mode ( ) ;
914
let want = if exec { mode | 0o111 } else { mode & ! 0o111 } ;
915
if want ! = mode {
916
perms.set_mode ( want) ;
917
fs:: set_permissions( path, perms) ? ;
918
}
919
Ok ( ( ) )
920
}
921
# [ cfg ( not ( unix ) ) ]
922
fn set_executable ( _path : & Path, _exec : bool ) -> std:: io:: Result< ( ) > {
923
Ok ( ( ) )
924
}
925
926
# [ cfg ( unix ) ]
927
fn is_executable ( m : & fs:: Metadata) -> bool {
928
use std:: os:: unix:: fs:: PermissionsExt;
929
m.permissions ( ) .mode ( ) & 0o111 ! = 0
930
}
931
# [ cfg ( not ( unix ) ) ]
932
fn is_executable ( _m : & fs:: Metadata) -> bool {
933
false
934
}
935
936
fn op_ref ( k : OpKey) -> OpRef {
937
OpRef { change: k.0 , index: k.1 }
938
}
939
940
fn split_path ( path : & str ) -> ( & str , & str ) {
941
match path.rfind ( ' /' ) {
942
Some ( i) => ( & path[ .. i] , & path[ i + 1 .. ] ) ,
943
None => ( " " , path) ,
944
}
945
}