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