ff0af1ee unknown 1 month ago
1
2
3
use anyhow:: { bail, Context, Result } ;
4
use clap:: { Parser, Subcommand} ;
1983686f Andy McCall 1 month ago ↶
5
use oxy_core:: repo:: { ChangeStatus, Repo, SignatureStatus} ;
ff0af1ee unknown 1 month ago
6
use std:: path:: PathBuf;
7
8
# [ derive ( Parser ) ]
9
# [ command ( name = " oxy" , version, about = " Oxygen: breathing fresh air into version control" ) ]
10
struct Cli {
11
#[command(subcommand)]
12
cmd : Cmd,
13
}
14
15
# [ derive ( Subcommand ) ]
16
enum Cmd {
17
18
Init { path: Option< PathBuf> } ,
5b9c6ad4 Andy McCall 1 month ago ↶
19
20
Clone { url: String , dest: Option< PathBuf> } ,
21
22
Remote {
23
# [ command ( subcommand ) ]
24
cmd: RemoteCmd,
25
} ,
26
27
Fetch { remote: Option< String> } ,
28
29
Pull { remote: Option< String> } ,
30
31
Push {
32
remote: Option< String> ,
33
34
# [ arg ( long ) ]
35
force: bool ,
36
} ,
ff0af1ee unknown 1 month ago
37
38
Status,
39
40
Commit {
41
# [ arg ( short, long ) ]
42
message: String ,
43
44
# [ arg ( long = " depends-on" ) ]
45
depends_on: Vec< String> ,
46
} ,
47
48
Log,
49
50
Diff,
51
52
Show { change: String } ,
f9c98dee unknown 1 month ago ↶
53
54
Checkout {
55
changes: Vec< String> ,
56
57
# [ arg ( long ) ]
58
discard: bool ,
59
} ,
20247a03 Andy McCall 1 month ago ↶
60
61
Resolve,
750251b2 Andy McCall 1 month ago ↶
62
63
Serve {
64
65
# [ arg ( long, default_value = " ." ) ]
66
root: PathBuf,
67
# [ arg ( long, default_value = " 127.0.0.1:8890" ) ]
68
bind: String ,
69
70
# [ arg ( long = " token" ) ]
71
tokens: Vec< String> ,
72
} ,
ff0af1ee unknown 1 month ago
73
74
Undo,
1983686f Andy McCall 1 month ago ↶
75
76
Identity {
77
# [ command ( subcommand ) ]
78
cmd: IdentityCmd,
79
} ,
80
81
Verify { change: Option< String> } ,
750251b2 Andy McCall 1 month ago ↶
82
1983686f Andy McCall 1 month ago ↶
83
Config { key: String , value: String } ,
ff0af1ee unknown 1 month ago
84
85
# [ command ( name = " op" ) ]
86
Op {
87
# [ command ( subcommand ) ]
88
cmd: OpCmd,
89
} ,
90
}
91
92
# [ derive ( Subcommand ) ]
93
enum OpCmd {
94
Log,
95
}
96
1983686f Andy McCall 1 month ago ↶
97
# [ derive ( Subcommand ) ]
5b9c6ad4 Andy McCall 1 month ago ↶
98
enum RemoteCmd {
99
Add { name: String , url: String } ,
100
Remove { name: String } ,
101
List,
102
}
103
104
# [ derive ( Subcommand ) ]
1983686f Andy McCall 1 month ago ↶
105
enum IdentityCmd {
106
107
Create {
108
# [ arg ( long ) ]
109
name: Option< String> ,
110
# [ arg ( long ) ]
111
email: Option< String> ,
112
} ,
113
114
Show,
115
}
116
117
fn signature_line ( repo : & Repo, h : & oxy_core:: hash:: ChangeHash) -> Result< String> {
5b9c6ad4 Andy McCall 1 month ago ↶
118
Ok ( match repo.authenticate_change ( h) ? {
1983686f Andy McCall 1 month ago ↶
119
SignatureStatus:: Unsigned => " unsigned" .to_string ( ) ,
5b9c6ad4 Andy McCall 1 month ago ↶
120
SignatureStatus:: Valid { key, name, email, sealed } => {
121
format! ( " {} by {name} <{email} > key {} " , if sealed { " sealed" } else { " signed" } , key.short ( 12 ) )
122
}
1983686f Andy McCall 1 month ago ↶
123
SignatureStatus:: Invalid( why) => format! ( " INVALID signature: {why} " ) ,
124
} )
125
}
126
ff0af1ee unknown 1 month ago
127
fn main ( ) {
5b1114a0 Andy McCall 1 month ago ↶
128
129
130
# [ cfg ( unix ) ]
131
unsafe {
132
libc:: signal( libc:: SIGPIPE , libc:: SIG_DFL ) ;
133
}
ff0af1ee unknown 1 month ago
134
match run ( ) {
135
Ok ( code) => std:: process:: exit( code) ,
136
Err ( e) => {
137
eprintln! ( " error: {e:#}" ) ;
138
std:: process:: exit( 1 ) ;
139
}
140
}
141
}
142
143
fn open ( ) -> Result< Repo> {
144
Repo:: open( & std:: env:: current_dir( ) ? ) .context ( " opening repository" )
145
}
146
147
fn short_id ( c : & oxy_core:: objects:: Change) -> String {
148
c.change_id.short ( 8 )
149
}
150
151
fn when ( ms : u64 ) -> String {
152
let secs = ( ms / 1000 ) as i64 ;
153
let days = secs.div_euclid ( 86_400 ) ;
154
let rem = secs.rem_euclid ( 86_400 ) ;
155
156
let z = days + 719_468 ;
157
let era = z.div_euclid ( 146_097 ) ;
158
let doe = z - era * 146_097 ;
159
let yoe = ( doe - doe / 1460 + doe / 36_524 - doe / 146_096 ) / 365 ;
160
let y = yoe + era * 400 ;
161
let doy = doe - ( 365 * yoe + yoe / 4 - yoe / 100 ) ;
162
let mp = ( 5 * doy + 2 ) / 153 ;
163
let d = doy - ( 153 * mp + 2 ) / 5 + 1 ;
164
let m = if mp < 10 { mp + 3 } else { mp - 9 } ;
165
let y = if m < = 2 { y + 1 } else { y } ;
166
format! ( " {y:04} -{m:02} -{d:02} {:02} :{:02} :{:02} Z" , rem / 3600 , ( rem % 3600 ) / 60 , rem % 60 )
167
}
168
169
fn run ( ) -> Result< i32 > {
170
let cli = Cli:: parse( ) ;
171
match cli.cmd {
172
Cmd:: Init { path } => {
173
let path = path.unwrap_or ( std:: env:: current_dir( ) ? ) ;
174
let repo = Repo:: init( & path) ? ;
175
println! ( " Initialized oxy repository in {} " , repo.root ( ) .display ( ) ) ;
176
println! ( " Bookmark: {} Author: {} <{} >" , repo.config.bookmark, repo.config.author_name, repo.config.author_email) ;
177
Ok ( 0 )
178
}
5b9c6ad4 Andy McCall 1 month ago ↶
179
Cmd:: Clone { url, dest } => {
180
let dest = dest.unwrap_or_else ( | | {
181
let base = url.trim_end_matches ( ' /' ) .rsplit ( ' /' ) .next ( ) .unwrap_or ( " repo" ) ;
182
PathBuf:: from( base)
183
} ) ;
184
let repo = Repo:: clone( & url, & dest) ? ;
185
let heads = repo.bookmark_heads ( ) ? ;
186
println! ( " Cloned into {} ({} on {} : {} head{} )" , repo.root ( ) .display ( ) , url, repo.config.bookmark, heads.len ( ) , if heads.len ( ) = = 1 { " " } else { " s" } ) ;
187
Ok ( 0 )
188
}
189
Cmd:: Remote { cmd } => {
190
let mut repo = open ( ) ? ;
191
match cmd {
192
RemoteCmd:: Add { name, url } => {
193
repo.config.set ( & format! ( " remote.{name} .url" ) , & url) ? ;
194
repo.save_config ( ) ? ;
195
println! ( " Added remote {name} = {url} " ) ;
196
}
197
RemoteCmd:: Remove { name } => {
198
repo.config.set ( & format! ( " remote.{name} .url" ) , " " ) ? ;
199
repo.save_config ( ) ? ;
200
println! ( " Removed remote {name} " ) ;
201
}
202
RemoteCmd:: List => {
203
let view = repo.view ( ) ? ;
204
for ( name, url) in & repo.config.remotes {
205
println! ( " {name} \t {url} " ) ;
206
if let Some ( bms) = view.remotes.get ( name) {
207
for ( bm, st) in bms {
208
let heads = repo.store ( ) .get_state ( st) .map ( | s | s.heads.len ( ) ) .unwrap_or ( 0 ) ;
209
println! ( " {bm} : {heads} head{} " , if heads = = 1 { " " } else { " s" } ) ;
210
}
211
}
212
}
213
}
214
}
215
Ok ( 0 )
216
}
217
Cmd:: Fetch { remote } => {
218
let repo = open ( ) ? ;
219
let remote = remote.unwrap_or_else ( | | " origin" .into ( ) ) ;
220
for ( name, heads, n) in repo.fetch ( & remote) ? {
221
println! ( " {remote} /{name} : {} head{} , {n} new change{} " , heads.len ( ) , if heads.len ( ) = = 1 { " " } else { " s" } , if n = = 1 { " " } else { " s" } ) ;
222
}
223
Ok ( 0 )
224
}
225
Cmd:: Pull { remote } => {
226
let repo = open ( ) ? ;
227
let remote = remote.unwrap_or_else ( | | " origin" .into ( ) ) ;
228
let r = repo.pull ( & remote) ? ;
229
if ! r.changed {
230
println! ( " Already up to date." ) ;
231
} else {
232
println! ( " {} now has {} head{} {} " , repo.config.bookmark, r.heads.len ( ) , if r.heads.len ( ) = = 1 { " " } else { " s" } , if r.checked_out { " ; working copy updated" } else { " ; run `oxy checkout` to update the working copy" } ) ;
233
let snap = repo.snapshot ( " oxy pull" ) ? ;
234
if snap.base_snapshot.is_conflicted ( ) {
235
println! ( " The result has conflicts; see oxy status." ) ;
236
}
237
}
238
Ok ( 0 )
239
}
240
Cmd:: Push { remote, force } => {
241
let repo = open ( ) ? ;
242
let remote = remote.unwrap_or_else ( | | " origin" .into ( ) ) ;
243
let mut code = 0 ;
244
for r in repo.push ( & remote, force) ? {
245
if r.ok {
246
println! ( " {remote} /{} : updated" , r.name) ;
247
} else {
248
println! ( " {remote} /{} : rejected: {} " , r.name, r.reason) ;
249
code = 1 ;
250
}
251
}
252
Ok ( code)
253
}
ff0af1ee unknown 1 month ago
254
Cmd:: Status => {
255
let repo = open ( ) ? ;
256
let snap = repo.snapshot ( " oxy status" ) ? ;
257
let heads = repo.bookmark_heads ( ) ? ;
258
println! (
259
" Working copy: {} on {} ({} head{} )" ,
260
snap.wc.change_id.short ( 8 ) ,
261
repo.config.bookmark,
262
heads.len ( ) ,
263
if heads.len ( ) = = 1 { " " } else { " s" }
264
) ;
265
if snap.statuses.is_empty ( ) {
266
println! ( " The working copy is clean." ) ;
267
}
268
for s in & snap.statuses {
269
let tag = match s.status {
270
ChangeStatus:: Added => " A" ,
271
ChangeStatus:: Modified => " M" ,
272
ChangeStatus:: Removed => " D" ,
273
ChangeStatus:: ModeChanged => " X" ,
274
ChangeStatus:: ConflictEdited => " C!" ,
20247a03 Andy McCall 1 month ago ↶
275
ChangeStatus:: Resolved => " R" ,
ff0af1ee unknown 1 month ago
276
} ;
277
println! ( " {tag:2} {} " , s.path) ;
278
}
279
let mut code = 0 ;
280
if snap.base_snapshot.is_conflicted ( ) {
20247a03 Andy McCall 1 month ago ↶
281
println! ( " \n Conflicts in the base state (edit the marked files, or the .oxy-conflict-N candidates, then commit):" ) ;
ff0af1ee unknown 1 month ago
282
for c in & snap.base_snapshot.tree.conflicts {
283
println! ( " tree: {c:?} " ) ;
284
}
285
for ( p, f) in & snap.base_snapshot.files {
286
if f.is_conflicted ( ) {
20247a03 Andy McCall 1 month ago ↶
287
let state = match snap.statuses.iter ( ) .find ( | s | s.path = = * p ) .map ( | s | s.status ) {
288
Some ( ChangeStatus:: Resolved) => " resolved in working copy" ,
289
Some ( ChangeStatus:: ConflictEdited) => " markers remain" ,
290
_ => " unresolved" ,
291
} ;
292
println! ( " {p} ({state} )" ) ;
ff0af1ee unknown 1 month ago
293
}
294
}
295
code = 2 ;
296
}
297
Ok ( code)
298
}
299
Cmd:: Commit { message, depends_on } => {
300
let repo = open ( ) ? ;
301
let mut deps = Vec :: new( ) ;
302
for d in & depends_on {
303
deps.push ( repo.resolve_change ( d) ? ) ;
304
}
305
let h = repo.commit ( & message, & deps) ? ;
306
let c = repo.store ( ) .get_change ( & h) ? ;
307
println! ( " Committed {} #{} : {} " , short_id ( & c) , h.short ( 12 ) , message.lines ( ) .next ( ) .unwrap_or ( " " ) ) ;
308
Ok ( 0 )
309
}
310
Cmd:: Log => {
311
let repo = open ( ) ? ;
312
repo.snapshot ( " oxy log" ) ? ;
313
for e in repo.log ( ) ? {
314
let c = & e.change;
315
let mut flags = Vec :: new( ) ;
316
if e.is_working_copy {
317
flags.push ( " working copy" ) ;
318
}
319
if e.is_head {
320
flags.push ( " head" ) ;
321
}
322
let flags = if flags.is_empty ( ) { String :: new( ) } else { format! ( " ({} )" , flags.join ( " , " ) ) } ;
323
println! ( " {} #{} {} " , short_id ( c) , e.hash.short ( 12 ) , flags) ;
324
println! ( " {} " , when ( c.hlc.wall_ms) ) ;
325
if ! c.deps.is_empty ( ) {
326
println! ( " deps: {} " , c.deps.iter ( ) .map ( | d | format! ( " #{} " , d.short ( 12 ) ) ) .collect:: < Vec< _ > > ( ) .join ( " " ) ) ;
327
}
328
let msg = if c.message.is_empty ( ) { " (no description)" } else { & c.message } ;
329
for line in msg.lines ( ) {
330
println! ( " {line} " ) ;
331
}
332
println! ( ) ;
333
}
334
Ok ( 0 )
335
}
336
Cmd:: Diff => {
337
let repo = open ( ) ? ;
338
let snap = repo.snapshot ( " oxy diff" ) ? ;
339
print! ( " {} " , oxy_core:: repo:: render_change( & snap.wc, & snap.base, & snap.base_snapshot) ) ;
340
Ok ( 0 )
341
}
342
Cmd:: Show { change } => {
343
let repo = open ( ) ? ;
344
repo.snapshot ( " oxy show" ) ? ;
345
let h = repo.resolve_change ( & change) ? ;
346
let c = repo.store ( ) .get_change ( & h) ? ;
347
println! ( " change {} #{} " , short_id ( & c) , h) ;
348
println! ( " kind: {:?} " , c.kind) ;
349
println! ( " date: {} " , when ( c.hlc.wall_ms) ) ;
350
println! ( " deps: {} " , c.deps.iter ( ) .map ( | d | format! ( " #{} " , d.short ( 12 ) ) ) .collect:: < Vec< _ > > ( ) .join ( " " ) ) ;
1983686f Andy McCall 1 month ago ↶
351
println! ( " sig: {} " , signature_line ( & repo, & h) ? ) ;
ff0af1ee unknown 1 month ago
352
println! ( " \n {} \n " , if c.message.is_empty ( ) { " (no description)" } else { & c.message } ) ;
353
print! ( " {} " , repo.describe_change ( & h) ? ) ;
354
Ok ( 0 )
355
}
f9c98dee unknown 1 month ago ↶
356
Cmd:: Checkout { changes, discard } => {
357
let repo = open ( ) ? ;
358
let heads = if changes.is_empty ( ) {
359
repo.bookmark_heads ( ) ?
360
} else {
361
changes.iter ( ) .map ( | c | repo.resolve_change ( c) ) .collect:: < Result< Vec< _ > , _ > > ( ) ?
362
} ;
363
let spec = if changes.is_empty ( ) { repo.config.bookmark.clone ( ) } else { changes.join ( " " ) } ;
364
let r = repo.checkout ( & heads, discard, & format! ( " oxy checkout {spec} " ) ) ? ;
365
println! ( " Working copy now on {spec} : {} file(s) written, {} removed" , r.written, r.removed) ;
366
if r.conflicted {
367
println! ( " This state has conflicts; see oxy status." ) ;
368
}
369
Ok ( 0 )
370
}
20247a03 Andy McCall 1 month ago ↶
371
Cmd:: Resolve => {
372
let repo = open ( ) ? ;
373
let snap = repo.snapshot ( " oxy resolve" ) ? ;
374
if ! snap.base_snapshot.is_conflicted ( ) {
375
println! ( " The working copy's base has no conflicts." ) ;
376
return Ok ( 0 ) ;
377
}
378
let mut remaining = 0 ;
379
for ( p, f) in & snap.base_snapshot.files {
380
if ! f.is_conflicted ( ) {
381
continue ;
382
}
383
match snap.statuses.iter ( ) .find ( | s | s.path = = * p ) .map ( | s | s.status ) {
384
Some ( ChangeStatus:: Resolved) => println! ( " resolved {p} " ) ,
385
Some ( ChangeStatus:: ConflictEdited) => {
386
remaining + = 1 ;
387
println! ( " markers {p} " ) ;
388
}
389
_ => {
390
remaining + = 1 ;
391
println! ( " unresolved {p} " ) ;
392
}
393
}
394
}
395
for c in & snap.base_snapshot.tree.conflicts {
396
remaining + = 1 ;
397
println! ( " tree {c:?} " ) ;
398
}
399
if remaining = = 0 {
400
println! ( " All conflicts are resolved; `oxy commit` will record the resolution." ) ;
401
Ok ( 0 )
402
} else {
403
Ok ( 2 )
404
}
405
}
750251b2 Andy McCall 1 month ago ↶
406
Cmd:: Serve { root, bind, tokens } => {
407
let listener = std:: net:: TcpListener:: bind( & bind) .with_context ( | | format! ( " binding {bind} " ) ) ? ;
408
let root = root.canonicalize ( ) ? ;
409
println! ( " Serving repositories under {} on http://{bind} /<repo>/oxy/v1/ ({} )" , root.display ( ) , if tokens.is_empty ( ) { " open: no push tokens configured" } else { " push requires a token" } ) ;
410
oxy_server:: run_blocking( listener, oxy_server:: ServerConfig { root, tokens } ) ? ;
411
Ok ( 0 )
412
}
ff0af1ee unknown 1 month ago
413
Cmd:: Undo => {
414
let repo = open ( ) ? ;
415
let op = repo.undo ( ) ? ;
416
println! ( " Undid the last operation; now at: {} " , op.command) ;
417
Ok ( 0 )
418
}
1983686f Andy McCall 1 month ago ↶
419
Cmd:: Identity { cmd: IdentityCmd:: Create { name, email } } => {
420
let mut repo = open ( ) ? ;
421
let name = name.unwrap_or_else ( | | repo.config.author_name.clone ( ) ) ;
422
let email = email.unwrap_or_else ( | | repo.config.author_email.clone ( ) ) ;
423
let ( key, _ ) = repo.create_identity ( & name, & email) ? ;
424
println! ( " Created identity {name} <{email} >" ) ;
425
println! ( " KeyId: {key} " ) ;
426
println! ( " Secret key stored under {} " , oxy_core:: repo:: default_keys_dir( ) .display ( ) ) ;
427
if ! repo.config.sign_changes {
428
println! ( " Changes will carry this author; run `oxy config core.sign change` to sign each commit." ) ;
429
}
430
Ok ( 0 )
431
}
432
Cmd:: Identity { cmd: IdentityCmd:: Show } => {
433
let repo = open ( ) ? ;
434
match repo.config.author_key {
435
None => println! ( " No identity configured; run `oxy identity create`." ) ,
436
Some ( key) => {
437
let id = repo.identity ( & key) ? ;
438
println! ( " {} <{} >" , id.name, id.email) ;
439
println! ( " KeyId: {key} " ) ;
440
println! ( " Created: {} " , when ( id.created) ) ;
441
println! ( " Secret: {} " , if repo.secret_key ( ) ? .is_some ( ) { " available" } else { " NOT FOUND in key directory" } ) ;
442
println! ( " Signing: {} " , if repo.config.sign_changes { " every change (core.sign = change)" } else { " at push via seals (core.sign = seal)" } ) ;
443
}
444
}
445
Ok ( 0 )
446
}
447
Cmd:: Verify { change } => {
448
let repo = open ( ) ? ;
449
let hashes: Vec< oxy_core:: hash:: ChangeHash> = match change {
450
Some ( c) => vec! [ repo.resolve_change ( & c) ? ] ,
451
None => repo.log ( ) ? .into_iter ( ) .filter ( | e | ! e.is_working_copy) .map ( | e | e.hash ) .collect ( ) ,
452
} ;
453
let mut bad = 0 ;
454
for h in hashes {
455
let c = repo.store ( ) .get_change ( & h) ? ;
456
let line = signature_line ( & repo, & h) ? ;
457
if line.starts_with ( " INVALID" ) {
458
bad + = 1 ;
459
}
460
println! ( " {} #{} {} " , short_id ( & c) , h.short ( 12 ) , line) ;
461
}
462
Ok ( if bad > 0 { 1 } else { 0 } )
463
}
464
Cmd:: Config { key, value } => {
465
let mut repo = open ( ) ? ;
466
repo.config.set ( & key, & value) ? ;
467
repo.save_config ( ) ? ;
468
println! ( " {key} = {value} " ) ;
469
Ok ( 0 )
470
}
ff0af1ee unknown 1 month ago
471
Cmd:: Op { cmd: OpCmd:: Log } => {
472
let repo = open ( ) ? ;
473
for ( i, ( r, op) ) in repo.op_log ( ) ? .into_iter ( ) .enumerate ( ) {
474
println! ( " {} {} {} {} " , if i = = 0 { " @ " } else { " " } , r.short ( 12 ) , when ( op.hlc.wall_ms) , op.command) ;
475
}
476
Ok ( 0 )
477
}
478
}
479
.map_err ( | e : anyhow:: Error| e )
480
.or_else ( | e | {
481
if e.to_string ( ) .is_empty ( ) {
482
bail! ( " unknown error" )
483
} else {
484
Err ( e)
485
}
486
} )
487
}