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} ;
98286406 Andy McCall 1 month ago ↶
13
use crate :: hash:: { hash_object, ChangeHash, KeyId, ObjectRef, ObjectType} ;
14
use crate :: identity:: { verify as verify_signature, Domain, Identity, SecretKey, Signature} ;
15
use crate :: ids:: { ChangeId, NodeId, WorkspaceId} ;
16
use crate :: objects:: { Blob, Change, ChangeKind, Edit, Hlc, Mode, NodeKind, OpRef, StateObject, TreeOp} ;
17
use crate :: oplog:: { Format1Op, Op, View, SNAPSHOT_SUFFIX } ;
18
use crate :: state:: { FileContent, Snapshot, State} ;
19
use crate :: store:: { Store, StoreError} ;
20
use crate :: tree:: OpKey;
21
use rand:: RngCore;
22
use std:: collections:: { BTreeMap, BTreeSet, HashMap} ;
23
use std:: fs;
24
use std:: io:: Write;
25
use std:: path:: { Path, PathBuf} ;
26
27
pub const OXY_DIR : & str = " .oxy" ;
28
pub const FORMAT_LINE : & str = " oxy repository format 2\n " ;
29
const FORMAT1_LINE : & str = " oxy repository format 1\n " ;
30
const BINARY_SNIFF : usize = 8 * 1024 ;
d02e545d Andy McCall 1 month ago ↶
31
32
pub const AUTOPACK_THRESHOLD : usize = 4096 ;
98286406 Andy McCall 1 month ago ↶
33
34
# [ derive ( Debug, thiserror::Error ) ]
35
pub enum RepoError {
36
# [ error ( " io: {0}" ) ]
37
Io( # [ from ] std:: io:: Error) ,
38
# [ error ( transparent ) ]
39
Store( # [ from ] StoreError) ,
40
# [ error ( transparent ) ]
41
Object( # [ from ] crate :: objects:: ObjectError) ,
42
# [ error ( transparent ) ]
43
State( # [ from ] crate :: state:: StateError) ,
44
# [ error ( " {0}" ) ]
45
Msg( String ) ,
46
}
47
48
pub type Result < T> = std:: result:: Result< T, RepoError> ;
49
50
fn fail < T> ( s : impl Into< String> ) -> Result< T> {
51
Err ( RepoError:: Msg( s.into ( ) ) )
52
}
53
54
mod checkout ;
252e8cb2 Andy McCall 1 month ago ↶
55
mod checkpoint ;
98286406 Andy McCall 1 month ago ↶
56
mod config ;
ba160f60 Andy McCall 1 month ago ↶
57
mod gc ;
79ab8528 Andy McCall 1 month ago ↶
58
mod git ;
614541c3 Andy McCall 1 month ago ↶
59
mod history ;
6d3eefbc Andy McCall 1 month ago ↶
60
mod hooks ;
98286406 Andy McCall 1 month ago ↶
61
mod identity ;
83e0677b Andy McCall 1 month ago ↶
62
mod policy ;
135212de Andy McCall 1 month ago ↶
63
mod prompt ;
98286406 Andy McCall 1 month ago ↶
64
mod snapshot ;
65
mod sync ;
132b884b Andy McCall 1 month ago ↶
66
mod tags ;
4e88e774 Andy McCall 1 month ago ↶
67
mod workspace ;
98286406 Andy McCall 1 month ago ↶
68
# [ cfg ( test ) ]
69
mod tests ;
70
71
pub use checkout:: CheckoutReport;
b3c5a59a Andy McCall 1 month ago ↶
72
pub use config:: { parse_size, read_config_file, Config} ;
ba160f60 Andy McCall 1 month ago ↶
73
pub use gc:: GcReport;
6d3eefbc Andy McCall 1 month ago ↶
74
pub use hooks:: { json_array, json_str, HookOutcome} ;
135212de Andy McCall 1 month ago ↶
75
pub use prompt:: { PromptInfo, DEFAULT_PROMPT_FORMAT } ;
c3069537 Andy McCall 1 month ago ↶
76
pub use git:: { GitExportReport, GitImportReport} ;
28fc23d7 Andy McCall 1 month ago ↶
77
pub use identity:: { Rotation, SignatureStatus} ;
78
pub use policy:: PolicyUpdate;
6e723b81 Andy McCall 1 month ago ↶
79
pub use snapshot:: { added_node_paths, has_conflict_markers, is_binary, referenced_changes, render_change, similarity, ChangeStatus, PathStatus, SnapshotResult} ;
83e0677b Andy McCall 1 month ago ↶
80
pub use sync:: { FetchReport, PullReport} ;
132b884b Andy McCall 1 month ago ↶
81
pub use tags:: TagInfo;
4e88e774 Andy McCall 1 month ago ↶
82
pub use workspace:: { SnapshotLock, WorkspaceInfo} ;
98286406 Andy McCall 1 month ago ↶
83
84
fn write_atomic ( path : & Path, data : & [u8 ]) -> Result< ( ) > {
85
let tmp = path.with_extension ( " tmp" ) ;
86
{
87
let mut f = fs:: File:: create( & tmp) ? ;
88
f.write_all ( data) ? ;
89
f.sync_all ( ) ? ;
90
}
91
fs:: rename( & tmp, path) ? ;
92
Ok ( ( ) )
93
}
94
95
pub struct LogEntry {
96
pub hash : ChangeHash,
2863456a Andy McCall 1 month ago ↶
97
98
pub change : crate:: state:: ChangeMeta,
98286406 Andy McCall 1 month ago ↶
99
pub is_working_copy : bool ,
100
pub is_head : bool ,
101
}
102
c3069537 Andy McCall 1 month ago ↶
103
104
pub ( crate ) struct FsEntry {
105
pub kind : NodeKind,
106
pub exec : bool ,
107
108
pub content : Vec< u8 > ,
98286406 Andy McCall 1 month ago ↶
109
}
110
111
pub struct Repo {
112
root : PathBuf,
113
oxy : PathBuf,
114
store : Store,
115
workspace : WorkspaceId,
116
keys_dir : PathBuf,
117
user_config : PathBuf,
118
pub config : Config,
119
}
120
121
122
123
pub fn default_home ( ) -> PathBuf {
124
if let Ok ( h) = std:: env:: var( " OXY_HOME" ) {
125
return PathBuf:: from( h) ;
126
}
127
if let Ok ( x) = std:: env:: var( " XDG_CONFIG_HOME" ) {
128
return PathBuf:: from( x) .join ( " oxy" ) ;
129
}
130
let home = std:: env:: var( " HOME" ) .unwrap_or_else ( | _| " ." .into ( ) ) ;
131
PathBuf:: from( home) .join ( " .config" ) .join ( " oxy" )
132
}
133
134
pub fn default_keys_dir ( ) -> PathBuf {
135
default_home ( ) .join ( " keys" )
136
}
137
138
pub fn default_user_config ( ) -> PathBuf {
139
default_home ( ) .join ( " config.toml" )
140
}
141
142
143
# [ derive ( Clone, Copy, Debug, PartialEq, Eq ) ]
144
pub enum IdentityScope {
145
146
User,
147
148
Repo,
149
}
150
151
impl Repo {
152
153
154
pub fn init ( root : & Path) -> Result< Repo> {
155
let mut id = [ 0 u8 ; 16 ] ;
156
rand:: rngs:: OsRng.fill_bytes ( & mut id) ;
157
Self :: init_with_id( root, id)
158
}
159
160
161
162
pub fn init_with_id ( root : & Path, id : [u8 ; 16]) -> Result< Repo> {
163
fs:: create_dir_all( root) ? ;
164
let root = root.canonicalize ( ) .unwrap_or_else ( | _| root.to_path_buf ( ) ) ;
165
let oxy = root.join ( OXY_DIR ) ;
166
if oxy.exists ( ) {
167
return fail ( format! ( " {} is already an oxy repository" , root.display ( ) ) ) ;
168
}
169
fs:: create_dir_all( oxy.join ( " ops" ) ) ? ;
170
write_atomic ( & oxy.join ( " format" ) , FORMAT_LINE .as_bytes ( ) ) ? ;
171
write_atomic ( & oxy.join ( " id" ) , hex:: encode( id) .as_bytes ( ) ) ? ;
172
let config = Config:: load( Some ( & default_user_config ( ) ) , & oxy.join ( " config" ) ) ? ;
173
config.save ( & oxy.join ( " config" ) ) ? ;
174
let workspace = WorkspaceId:: random( ) ;
175
write_atomic ( & oxy.join ( " workspace" ) , workspace.to_hex ( ) .as_bytes ( ) ) ? ;
176
let store = Store:: open( oxy.join ( " objects" ) ) ? ;
177
let repo = Repo { root, oxy, store, workspace, keys_dir: default_keys_dir ( ) , user_config: default_user_config ( ) , config } ;
4e88e774 Andy McCall 1 month ago ↶
178
repo.register_workspace ( & workspace, & repo.root) ? ;
98286406 Andy McCall 1 month ago ↶
179
180
let wc = repo.new_working_copy ( vec! [ ] , " " ) ;
181
let wc_hash = repo.store.put_change ( & wc) ? ;
182
let mut view = View:: default( ) ;
183
view.bookmarks.insert ( repo.config.bookmark.clone ( ) , repo.store.put_state ( & StateObject:: new( vec! [ ] ) ) ? ) ;
184
view.workspaces.insert ( workspace, wc_hash) ;
185
repo.record_op ( & [ ] , " oxy init" , view) ? ;
186
Ok ( repo)
187
}
188
189
190
pub fn open ( start : & Path) -> Result< Repo> {
191
let start = start.canonicalize ( ) ? ;
192
let mut dir: Option< & Path> = Some ( & start) ;
193
while let Some ( d) = dir {
194
let oxy = d.join ( OXY_DIR ) ;
4e88e774 Andy McCall 1 month ago ↶
195
if oxy.is_file ( ) {
196
197
let text = fs:: read_to_string( & oxy) ? ;
198
let Some ( ( shared, workspace) ) = workspace:: parse_workspace_file( & text) else {
199
return fail ( format! ( " {} is not a valid workspace file" , oxy.display ( ) ) ) ;
200
} ;
201
let format = fs:: read_to_string( shared.join ( " format" ) ) ? ;
202
if format ! = FORMAT_LINE {
203
return fail ( format! ( " unsupported repository format: {} " , format.trim ( ) ) ) ;
204
}
205
let config = Config:: load( Some ( & default_user_config ( ) ) , & shared.join ( " config" ) ) ? ;
206
let store = Store:: open( shared.join ( " objects" ) ) ? ;
207
return Ok ( Repo { root: d.to_path_buf ( ) , oxy: shared, store, workspace, keys_dir: default_keys_dir ( ) , user_config: default_user_config ( ) , config } ) ;
208
}
98286406 Andy McCall 1 month ago ↶
209
if oxy.is_dir ( ) {
210
let format = fs:: read_to_string( oxy.join ( " format" ) ) ? ;
211
let config = Config:: load( Some ( & default_user_config ( ) ) , & oxy.join ( " config" ) ) ? ;
212
let workspace = WorkspaceId:: from_hex( fs:: read_to_string( oxy.join ( " workspace" ) ) ? .trim ( ) )
213
.ok_or_else ( | | RepoError:: Msg( " corrupt .oxy/workspace" .into ( ) ) ) ? ;
214
let store = Store:: open( oxy.join ( " objects" ) ) ? ;
215
let repo = Repo { root: d.to_path_buf ( ) , oxy, store, workspace, keys_dir: default_keys_dir ( ) , user_config: default_user_config ( ) , config } ;
216
if format = = FORMAT1_LINE {
217
repo.migrate_format1 ( ) ? ;
218
} else if format ! = FORMAT_LINE {
219
return fail ( format! ( " unsupported repository format: {} " , format.trim ( ) ) ) ;
220
}
221
return Ok ( repo) ;
222
}
223
dir = d.parent ( ) ;
224
}
225
fail ( format! ( " no oxy repository found at or above {} " , start.display ( ) ) )
226
}
227
228
229
230
fn migrate_format1 ( & self ) -> Result< ( ) > {
231
let hex = fs:: read_to_string( self .oxy.join ( " ops" ) .join ( " head" ) ) ? ;
232
let head = ObjectRef:: from_hex( hex.trim ( ) ) .ok_or_else ( | | RepoError:: Msg( " corrupt .oxy/ops/head" .into ( ) ) ) ? ;
233
234
let mut chain: Vec< Format1Op> = Vec :: new( ) ;
235
let mut cur = Some ( head) ;
236
while let Some ( r) = cur {
237
let op = Op:: decode_format1( & self .store.get_typed ( & r, ObjectType:: Op) ? ) ? ;
238
cur = op.parents.first ( ) .copied ( ) ;
239
chain.push ( op) ;
240
}
241
chain.reverse ( ) ;
242
let mut new_parent: Option< ObjectRef> = None ;
243
for old in chain {
244
let mut view = View:: default( ) ;
245
for ( name, heads) in & old.view.bookmarks {
246
view.bookmarks.insert ( name.clone ( ) , self .store.put_state ( & StateObject:: new( heads.clone ( ) ) ) ? ) ;
247
}
248
view.workspaces = old.view.workspaces.clone ( ) ;
249
let op = Op { parents: new_parent.into_iter ( ) .collect ( ) , hlc: old.hlc, command: old.command, view } ;
250
new_parent = Some ( self .store.put ( ObjectType:: Op, & op.encode ( ) ) ? ) ;
251
}
252
write_atomic ( & self .oxy.join ( " ops" ) .join ( " head" ) , new_parent.unwrap ( ) .to_hex ( ) .as_bytes ( ) ) ? ;
253
write_atomic ( & self .oxy.join ( " format" ) , FORMAT_LINE .as_bytes ( ) ) ? ;
254
Ok ( ( ) )
d02e545d Andy McCall 1 month ago ↶
255
}
256
257
258
pub ( crate ) fn maybe_autopack ( & self ) -> Result< ( ) > {
259
if self .store.loose_count ( ) ? > AUTOPACK_THRESHOLD {
260
self .store.pack_loose ( ) ? ;
135212de Andy McCall 1 month ago ↶
261
self .store.merge_packs ( ) ? ;
d02e545d Andy McCall 1 month ago ↶
262
}
263
Ok ( ( ) )
264
}
265
98286406 Andy McCall 1 month ago ↶
266
267
pub fn id ( & self ) -> Result< [ u8 ; 16 ] > {
268
let path = self .oxy.join ( " id" ) ;
269
match fs:: read_to_string( & path) {
270
Ok ( h) => hex:: decode( h.trim ( ) ) .ok ( ) .and_then ( | v | v.try_into ( ) .ok ( ) ) .ok_or_else ( | | RepoError:: Msg( " corrupt .oxy/id" .into ( ) ) ) ,
271
Err ( e) if e.kind ( ) = = std:: io:: ErrorKind:: NotFound => {
272
let mut id = [ 0 u8 ; 16 ] ;
273
rand:: rngs:: OsRng.fill_bytes ( & mut id) ;
274
write_atomic ( & path, hex:: encode( id) .as_bytes ( ) ) ? ;
275
Ok ( id)
276
}
277
Err ( e) => Err ( e.into ( ) ) ,
278
}
279
}
280
281
pub fn root ( & self ) -> & Path {
282
& self .root
283
}
284
pub fn store ( & self ) -> & Store {
285
& self .store
286
}
287
pub fn workspace ( & self ) -> WorkspaceId {
288
self .workspace
289
}
290
291
292
293
pub fn with_home ( mut self , dir : impl Into< PathBuf> ) -> Repo {
294
let dir = dir.into ( ) ;
295
self .keys_dir = dir.join ( " keys" ) ;
296
self .user_config = dir.join ( " config.toml" ) ;
297
if let Ok ( c) = Config:: load( Some ( & self .user_config) , & self .oxy.join ( " config" ) ) {
298
self .config = c;
299
}
300
self
301
}
302
303
pub fn user_config_path ( & self ) -> & Path {
304
& self .user_config
305
}
306
307
308
pub fn set_user_config ( & mut self , key : & str , value : & str ) -> Result< ( ) > {
309
Config:: set_user( & self .user_config, key, value) ? ;
310
self .config = Config:: load( Some ( & self .user_config) , & self .oxy.join ( " config" ) ) ? ;
311
Ok ( ( ) )
312
}
313
314
pub fn save_config ( & self ) -> Result< ( ) > {
315
self .config.save ( & self .oxy.join ( " config" ) )
316
}
317
318
fn author ( & self ) -> KeyId {
319
self .config.author_key.unwrap_or ( ObjectRef:: ZERO )
320
}
321
}
322
323
impl Repo {
324
fn new_working_copy ( & self , deps : Vec< ChangeHash> , message : & str ) -> Change {
325
let mut c = Change {
326
change_id: ChangeId:: random( ) ,
327
deps,
328
author: self .author ( ) ,
329
hlc: Hlc:: now( ) ,
330
message: message.into ( ) ,
331
tree_ops: vec! [ ] ,
332
edits: vec! [ ] ,
333
kind: ChangeKind:: WorkingCopy,
334
} ;
335
c.normalize ( ) ;
336
c
337
}
338
339
340
341
pub fn head_op ( & self ) -> Result< ( ObjectRef, Op) > {
342
let hex = fs:: read_to_string( self .oxy.join ( " ops" ) .join ( " head" ) ) ? ;
343
let r = ObjectRef:: from_hex( hex.trim ( ) ) .ok_or_else ( | | RepoError:: Msg( " corrupt .oxy/ops/head" .into ( ) ) ) ? ;
344
let op = Op:: decode( & self .store.get_typed ( & r, ObjectType:: Op) ? ) ? ;
345
Ok ( ( r, op) )
346
}
347
348
pub fn view ( & self ) -> Result< View> {
349
Ok ( self .head_op ( ) ? .1. view)
350
}
351
352
fn record_op ( & self , parents : & [ObjectRef], command : & str , view : View) -> Result< ObjectRef> {
353
let op = Op { parents: parents.to_vec ( ) , hlc: Hlc:: now( ) , command: command.into ( ) , view } ;
354
let r = self .store.put ( ObjectType:: Op, & op.encode ( ) ) ? ;
355
write_atomic ( & self .oxy.join ( " ops" ) .join ( " head" ) , r.to_hex ( ) .as_bytes ( ) ) ? ;
356
Ok ( r)
357
}
358
359
pub fn op_log ( & self ) -> Result< Vec< ( ObjectRef, Op) > > {
360
let mut out = Vec :: new( ) ;
361
let ( mut r, mut op) = self .head_op ( ) ? ;
362
loop {
363
let next = op.parents.first ( ) .copied ( ) ;
364
out.push ( ( r, op) ) ;
365
match next {
366
Some ( p) => {
367
r = p;
368
op = Op:: decode( & self .store.get_typed ( & r, ObjectType:: Op) ? ) ? ;
369
}
370
None => break ,
371
}
372
}
373
Ok ( out)
374
}
375
376
377
378
379
pub fn undo ( & self ) -> Result< Op> {
380
let ( _ , mut op) = self .head_op ( ) ? ;
381
while op.is_snapshot ( ) {
382
let Some ( parent) = op.parents.first ( ) else { break } ;
383
op = Op:: decode( & self .store.get_typed ( parent, ObjectType:: Op) ? ) ? ;
384
}
385
let Some ( parent) = op.parents.first ( ) else {
386
return fail ( " nothing to undo" ) ;
387
} ;
388
let parent_op = Op:: decode( & self .store.get_typed ( parent, ObjectType:: Op) ? ) ? ;
389
write_atomic ( & self .oxy.join ( " ops" ) .join ( " head" ) , parent.to_hex ( ) .as_bytes ( ) ) ? ;
135212de Andy McCall 1 month ago ↶
390
self .drop_manifest ( ) ;
98286406 Andy McCall 1 month ago ↶
391
Ok ( parent_op)
392
}
393
394
395
pub fn last_command ( & self ) -> Result< Op> {
396
let ( _ , mut op) = self .head_op ( ) ? ;
397
while op.is_snapshot ( ) {
398
let Some ( parent) = op.parents.first ( ) else { break } ;
399
op = Op:: decode( & self .store.get_typed ( parent, ObjectType:: Op) ? ) ? ;
400
}
401
Ok ( op)
402
}
403
404
405
252e8cb2 Andy McCall 1 month ago ↶
406
98286406 Andy McCall 1 month ago ↶
407
pub fn load_state ( & self , heads : & [ChangeHash]) -> Result< State> {
252e8cb2 Andy McCall 1 month ago ↶
408
if let Some ( st) = self .load_state_checkpointed ( heads) ? {
409
return Ok ( st) ;
410
}
411
self .load_state_full ( heads)
412
}
413
414
415
pub fn load_state_full ( & self , heads : & [ChangeHash]) -> Result< State> {
98286406 Andy McCall 1 month ago ↶
416
let mut state = State:: new( ) ;
417
let mut stack: Vec< ChangeHash> = heads.to_vec ( ) ;
418
let mut seen: BTreeSet< ChangeHash> = BTreeSet:: new( ) ;
419
while let Some ( h) = stack.pop ( ) {
420
if ! seen.insert ( h) {
421
continue ;
422
}
423
let c = self .store.get_change ( & h) ? ;
424
stack.extend ( c.deps.iter ( ) .copied ( ) ) ;
425
state.add_unchecked ( c) ;
426
}
427
state.validate ( ) ? ;
428
Ok ( state)
429
}
430
431
pub fn working_copy ( & self ) -> Result< ( ChangeHash, Change) > {
432
let view = self .view ( ) ? ;
433
let h = * view
434
.workspaces
435
.get ( & self .workspace)
436
.ok_or_else ( | | RepoError:: Msg( " this workspace is not in the current view" .into ( ) ) ) ? ;
437
Ok ( ( h, self .store.get_change ( & h) ? ) )
438
}
439
440
441
pub fn heads_of ( & self , view : & View, bookmark : & str ) -> Result< Vec< ChangeHash> > {
442
match view.bookmarks.get ( bookmark) {
443
Some ( st) => Ok ( self .store.get_state ( st) ? .heads) ,
444
None => Ok ( vec! [ ] ) ,
445
}
446
}
447
448
pub fn bookmark_heads ( & self ) -> Result< Vec< ChangeHash> > {
449
self .heads_of ( & self .view ( ) ? , & self .config.bookmark)
450
}
451
452
453
454
fn blob_for ( content : & [u8 ]) -> Blob {
455
Blob {
456
length: content.len ( ) as u64 ,
457
chunks: crate :: chunk:: chunks( content) .into_iter ( ) .map ( | c | hash_object ( ObjectType:: Chunk, c) ) .collect ( ) ,
458
}
459
}
460
461
pub fn blob_ref ( content : & [u8 ]) -> ObjectRef {
462
Self :: blob_for( content) .hash ( )
463
}
464
465
pub fn store_blob ( & self , content : & [u8 ]) -> Result< ObjectRef> {
466
for c in crate :: chunk:: chunks( content) {
467
self .store.put_chunk ( c) ? ;
468
}
469
Ok ( self .store.put ( ObjectType:: Blob, & Self :: blob_for( content) .encode ( ) ) ? )
470
}
471
b3c5a59a Andy McCall 1 month ago ↶
472
473
98286406 Andy McCall 1 month ago ↶
474
pub fn read_blob ( & self , r : & ObjectRef) -> Result< Vec< u8 > > {
b3c5a59a Andy McCall 1 month ago ↶
475
if ! self .store.has ( r) & & self .lazy_remote ( r) .is_some ( ) {
476
self .fetch_lazy_blob ( r) ? ;
477
}
98286406 Andy McCall 1 month ago ↶
478
let blob = Blob:: decode( & self .store.get_typed ( r, ObjectType:: Blob) ? ) ? ;
479
let mut out = Vec :: with_capacity( blob.length as usize ) ;
480
for c in & blob.chunks {
481
out.extend ( self .store.get_typed ( c, ObjectType:: Chunk) ? ) ;
482
}
483
Ok ( out)
484
}
485
486
487
cdb5beba Andy McCall 1 month ago ↶
488
489
pub fn disk_attributes ( & self ) -> crate:: attributes:: Attributes {
490
let mut files: Vec< ( String, Vec< u8 > ) > = Vec :: new( ) ;
491
if let Ok ( b) = fs:: read( self .root.join ( " .oxyattributes" ) ) {
492
files.push ( ( " .oxyattributes" .into ( ) , b) ) ;
493
}
494
let mut wb = ignore:: WalkBuilder:: new( & self .root) ;
495
wb.hidden ( false ) .git_ignore ( false ) .git_global ( false ) .git_exclude ( false ) .require_git ( false ) .follow_links ( false ) .add_custom_ignore_filename ( " .oxyignore" ) ;
496
wb.filter_entry ( | e | e.file_name ( ) ! = OXY_DIR ) ;
497
for entry in wb.build ( ) .flatten ( ) {
498
if entry.file_name ( ) = = " .oxyattributes" & & entry.path ( ) ! = self .root.join ( " .oxyattributes" ) {
499
if let Ok ( b) = fs:: read( entry.path ( ) ) {
500
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 ( " /" ) ;
501
files.push ( ( rel, b) ) ;
502
}
503
}
504
}
505
crate :: attributes:: Attributes:: from_files( files.iter ( ) .map ( | ( p , c ) | ( p.as_str ( ) , c.as_slice ( ) ) ) )
506
}
507
508
509
pub fn state_attributes ( snapshot : & Snapshot) -> crate:: attributes:: Attributes {
510
crate :: attributes:: Attributes:: from_files( snapshot.files.iter ( ) .filter_map ( | ( p , f ) | match & f.content {
511
FileContent:: Text { bytes, .. } if p = = " .oxyattributes" | | p.ends_with ( " /.oxyattributes" ) => Some ( ( p.as_str ( ) , bytes.as_slice ( ) ) ) ,
512
_ => None ,
513
} ) )
514
}
515
516
517
pub fn store_blob_for ( & self , content : & [u8 ], nocompress : bool ) -> Result< ObjectRef> {
518
if ! nocompress {
519
return self .store_blob ( content) ;
520
}
521
for c in crate :: chunk:: chunks( content) {
522
self .store.put_chunk_nocompress ( c) ? ;
523
}
524
Ok ( self .store.put ( ObjectType:: Blob, & Self :: blob_for( content) .encode ( ) ) ? )
525
}
526
527
fn locks_dir ( & self ) -> PathBuf {
528
self .oxy.join ( " locks" )
529
}
530
531
532
pub fn is_locked ( & self , path : & str ) -> bool {
533
self .locks_dir ( ) .join ( path.replace ( ' /' , " %2F" ) ) .exists ( )
534
}
535
536
537
pub fn lock_path ( & self , path : & str ) -> Result< ( ) > {
538
let attrs = self .disk_attributes ( ) ;
539
if ! attrs.lookup ( path) .lockable {
540
return fail ( format! ( " {path} is not marked lockable in .oxyattributes" ) ) ;
541
}
542
fs:: create_dir_all( self .locks_dir ( ) ) ? ;
543
write_atomic ( & self .locks_dir ( ) .join ( path.replace ( ' /' , " %2F" ) ) , std:: process:: id( ) .to_string ( ) .as_bytes ( ) ) ? ;
544
set_readonly ( & self .root.join ( path) , false ) ? ;
545
Ok ( ( ) )
546
}
547
548
549
pub fn unlock_path ( & self , path : & str ) -> Result< ( ) > {
550
let _ = fs:: remove_file( self .locks_dir ( ) .join ( path.replace ( ' /' , " %2F" ) ) ) ;
551
if self .root.join ( path) .exists ( ) {
552
set_readonly ( & self .root.join ( path) , true ) ? ;
553
}
554
Ok ( ( ) )
555
}
556
7421309f Andy McCall 1 month ago ↶
557
558
pub fn in_scope ( & self , path : & str ) -> bool {
559
self .config.sparse_paths.is_empty ( ) | | self .config.sparse_paths.iter ( ) .any ( | p | path = = p | | path.starts_with ( & format! ( " {p} /" ) ) )
560
}
561
98286406 Andy McCall 1 month ago ↶
562
fn walk_working_copy ( & self ) -> Result< BTreeMap< String, FsEntry> > {
563
let mut wb = ignore:: WalkBuilder:: new( & self .root) ;
564
wb.hidden ( false )
565
.git_ignore ( false )
566
.git_global ( false )
567
.git_exclude ( false )
568
.require_git ( false )
569
.follow_links ( false )
570
.add_custom_ignore_filename ( " .oxyignore" ) ;
571
wb.filter_entry ( | e | e.file_name ( ) ! = OXY_DIR ) ;
572
let mut out = BTreeMap:: new( ) ;
573
for entry in wb.build ( ) {
574
let entry = entry.map_err ( | e | RepoError:: Msg( e.to_string ( ) ) ) ? ;
575
let path = entry.path ( ) ;
576
if path = = self .root {
577
continue ;
578
}
579
let rel = path.strip_prefix ( & self .root) .unwrap ( ) ;
580
let rel: String = rel.components ( ) .map ( | c | c.as_os_str ( ) .to_string_lossy ( ) .to_string ( ) ) .collect:: < Vec< _ > > ( ) .join ( " /" ) ;
581
let Some ( ft) = entry.file_type ( ) else { continue } ;
7421309f Andy McCall 1 month ago ↶
582
if ft.is_dir ( ) | | ! self .in_scope ( & rel) {
98286406 Andy McCall 1 month ago ↶
583
continue ;
584
}
585
if ft.is_symlink ( ) {
586
let target = fs:: read_link( path) ? ;
587
out.insert ( rel, FsEntry { kind: NodeKind:: Symlink, exec: false , content: target.to_string_lossy ( ) .as_bytes ( ) .to_vec ( ) } ) ;
588
} else if ft.is_file ( ) {
589
let exec = is_executable ( & entry.metadata ( ) .map_err ( | e | RepoError:: Msg( e.to_string ( ) ) ) ? ) ;
590
out.insert ( rel, FsEntry { kind: NodeKind:: File, exec, content: fs:: read( path) ? } ) ;
591
}
592
}
593
Ok ( out)
594
}
595
}
596
597
impl Repo {
598
599
pub fn log ( & self ) -> Result< Vec< LogEntry> > {
600
let ( wc_hash, _ ) = self .working_copy ( ) ? ;
601
let mut heads = self .bookmark_heads ( ) ? ;
602
heads.push ( wc_hash) ;
603
let state = self .load_state ( & heads) ? ;
604
let head_set: BTreeSet< ChangeHash> = self .bookmark_heads ( ) ? .into_iter ( ) .collect ( ) ;
252e8cb2 Andy McCall 1 month ago ↶
605
let mut entries: Vec< LogEntry> = Vec :: new( ) ;
2863456a Andy McCall 1 month ago ↶
606
for ( h, meta) in state.changes ( ) {
607
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 ↶
608
}
98286406 Andy McCall 1 month ago ↶
609
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) ) ) ;
610
Ok ( entries)
611
}
612
132b884b Andy McCall 1 month ago ↶
613
614
615
pub fn resolve_heads ( & self , spec : & str ) -> Result< Vec< ChangeHash> > {
616
if spec = = self .config.bookmark {
617
return self .bookmark_heads ( ) ;
618
}
619
if let Some ( heads) = self .tag_heads ( spec) ? {
620
return Ok ( heads) ;
621
}
622
Ok ( vec! [ self .resolve_change ( spec) ? ] )
623
}
624
98286406 Andy McCall 1 month ago ↶
625
626
pub fn resolve_change ( & self , spec : & str ) -> Result< ChangeHash> {
627
if spec = = " @" {
628
return Ok ( self .working_copy ( ) ? .0 ) ;
629
}
630
if spec.is_empty ( ) | | spec = = " #" {
631
return fail ( " empty change specifier" ) ;
632
}
633
let entries = self .log ( ) ? ;
634
let matches: Vec< & LogEntry> = if let Some ( hp) = spec.strip_prefix ( ' #' ) {
635
entries.iter ( ) .filter ( | e | e.hash.starts_with_hex ( hp) ) .collect ( )
636
} else {
637
entries.iter ( ) .filter ( | e | e.change.change_id.starts_with_hex ( spec) ) .collect ( )
638
} ;
639
match matches.len ( ) {
640
1 => Ok ( matches[ 0 ] .hash) ,
641
0 => fail ( format! ( " no change matches {spec:?} " ) ) ,
642
_ => {
643
let ids: BTreeSet< ChangeId> = matches.iter ( ) .map ( | e | e.change.change_id ) .collect ( ) ;
644
if ids.len ( ) = = 1 {
645
fail ( format! ( " change {spec} is divergent: {} variants" , matches.len ( ) ) )
646
} else {
647
fail ( format! ( " {spec:?} is ambiguous" ) )
648
}
649
}
650
}
651
}
652
653
654
pub fn describe_change ( & self , hash : & ChangeHash) -> Result< String> {
655
let change = self .store.get_change ( hash) ? ;
656
let base = self .load_state ( & change.deps) ? ;
657
let base_snapshot = base.materialize ( ) ;
658
Ok ( render_change ( & change, & base, & base_snapshot) )
659
}
660
}
661
662
fn remove_any ( path : & Path) -> std:: io:: Result< ( ) > {
663
let m = fs:: symlink_metadata( path) ? ;
664
if m.is_dir ( ) { fs:: remove_dir_all( path) } else { fs:: remove_file( path) }
665
}
666
cdb5beba Andy McCall 1 month ago ↶
667
fn set_readonly ( path : & Path, readonly : bool ) -> std:: io:: Result< ( ) > {
668
let mut perms = fs:: metadata( path) ? .permissions ( ) ;
669
perms.set_readonly ( readonly) ;
670
fs:: set_permissions( path, perms)
671
}
672
98286406 Andy McCall 1 month ago ↶
673
# [ cfg ( unix ) ]
674
fn set_executable ( path : & Path, exec : bool ) -> std:: io:: Result< ( ) > {
675
use std:: os:: unix:: fs:: PermissionsExt;
676
let mut perms = fs:: metadata( path) ? .permissions ( ) ;
677
let mode = perms.mode ( ) ;
678
let want = if exec { mode | 0o111 } else { mode & ! 0o111 } ;
679
if want ! = mode {
680
perms.set_mode ( want) ;
681
fs:: set_permissions( path, perms) ? ;
682
}
683
Ok ( ( ) )
684
}
685
# [ cfg ( not ( unix ) ) ]
686
fn set_executable ( _path : & Path, _exec : bool ) -> std:: io:: Result< ( ) > {
687
Ok ( ( ) )
688
}
689
690
# [ cfg ( unix ) ]
691
fn is_executable ( m : & fs:: Metadata) -> bool {
692
use std:: os:: unix:: fs:: PermissionsExt;
693
m.permissions ( ) .mode ( ) & 0o111 ! = 0
694
}
695
# [ cfg ( not ( unix ) ) ]
696
fn is_executable ( _m : & fs:: Metadata) -> bool {
697
false
698
}
699
700
fn op_ref ( k : OpKey) -> OpRef {
701
OpRef { change: k.0 , index: k.1 }
702
}
703
704
fn split_path ( path : & str ) -> ( & str , & str ) {
705
match path.rfind ( ' /' ) {
706
Some ( i) => ( & path[ .. i] , & path[ i + 1 .. ] ) ,
707
None => ( " " , path) ,
708
}
709
}