79ab8528 Andy McCall 1 month ago ↶
1
2
3
4
5
6
7
8
9
10
use super :: * ;
11
use std:: process:: { Command, Stdio} ;
12
13
# [ derive ( Debug, Clone, Default, PartialEq, Eq ) ]
c3069537 Andy McCall 1 month ago ↶
14
pub struct GitImportReport {
15
pub commits : usize ,
16
pub merges : usize ,
17
pub skipped_submodules : usize ,
18
pub heads : usize ,
19
}
20
21
22
struct CatFile {
23
child : std:: process:: Child,
24
stdin : std:: process:: ChildStdin,
25
stdout : std:: io:: BufReader< std:: process:: ChildStdout> ,
26
}
27
28
impl CatFile {
29
fn start ( git_dir : & Path) -> Result< CatFile> {
30
let mut child = Command:: new( " git" )
31
.arg ( " -C" )
32
.arg ( git_dir)
33
.args ( [ " cat-file" , " --batch" ] )
34
.stdin ( Stdio:: piped( ) )
35
.stdout ( Stdio:: piped( ) )
36
.stderr ( Stdio:: null( ) )
37
.spawn ( )
38
.map_err ( | e | RepoError:: Msg( format! ( " running git cat-file: {e} " ) ) ) ? ;
39
let stdin = child.stdin.take ( ) .unwrap ( ) ;
40
let stdout = std:: io:: BufReader:: new( child.stdout.take ( ) .unwrap ( ) ) ;
41
Ok ( CatFile { child, stdin, stdout } )
42
}
43
44
fn read ( & mut self , sha : & str ) -> Result< Vec< u8 > > {
45
use std:: io:: { BufRead, Read} ;
46
writeln! ( self.stdin, " {sha} " ) ? ;
47
self .stdin.flush ( ) ? ;
48
let mut header = String :: new( ) ;
49
self .stdout.read_line ( & mut header) ? ;
50
let parts: Vec< & str > = header.trim_end ( ) .split ( ' ' ) .collect ( ) ;
51
if parts.len ( ) ! = 3 {
52
return fail ( format! ( " git cat-file: object {sha} not found" ) ) ;
53
}
54
let size: usize = parts[ 2 ] .parse ( ) .map_err ( | _| RepoError:: Msg( " git cat-file: bad size" .into ( ) ) ) ? ;
55
let mut body = vec! [ 0 u8 ; size] ;
56
self .stdout.read_exact ( & mut body) ? ;
57
let mut nl = [ 0 u8 ; 1 ] ;
58
self .stdout.read_exact ( & mut nl) ? ;
59
Ok ( body)
60
}
61
}
62
63
impl Drop for CatFile {
64
fn drop ( & mut self ) {
65
let _ = self .child.kill ( ) ;
66
let _ = self .child.wait ( ) ;
67
}
68
}
69
70
71
struct GitCommit {
72
sha : String,
73
parents : Vec< String> ,
74
author_secs : u64 ,
75
author : String,
76
message : String,
77
}
78
79
fn list_commits ( git_dir : & Path, branch : & str ) -> Result< Vec< GitCommit> > {
80
let out = Command:: new( " git" )
81
.arg ( " -C" )
82
.arg ( git_dir)
83
.args ( [ " log" , " --reverse" , " --topo-order" , " --format=%H%x00%P%x00%at%x00%an <%ae>%x00%B%x1e" , branch] )
84
.output ( )
85
.map_err ( | e | RepoError:: Msg( format! ( " running git log: {e} " ) ) ) ? ;
86
if ! out.status.success ( ) {
87
return fail ( format! ( " git log {branch} failed: {} " , String :: from_utf8_lossy( & out.stderr) .trim ( ) ) ) ;
88
}
89
let text = String :: from_utf8_lossy( & out.stdout) ;
90
let mut commits = Vec :: new( ) ;
91
for record in text.split ( ' \x1e ' ) {
92
let record = record.trim_start_matches ( ' \n ' ) ;
93
if record.trim ( ) .is_empty ( ) {
94
continue ;
95
}
96
let f: Vec< & str > = record.splitn ( 5 , ' \0 ' ) .collect ( ) ;
97
if f.len ( ) ! = 5 {
98
return fail ( " unexpected git log record" ) ;
99
}
100
commits.push ( GitCommit {
101
sha: f[ 0 ] .to_string ( ) ,
102
parents: f[ 1 ] .split_whitespace ( ) .map ( str :: to_string) .collect ( ) ,
103
author_secs: f[ 2 ] .parse ( ) .unwrap_or ( 0 ) ,
104
author: f[ 3 ] .to_string ( ) ,
105
message: f[ 4 ] .trim_end ( ) .to_string ( ) ,
106
} ) ;
107
}
108
Ok ( commits)
109
}
110
111
112
fn list_tree ( git_dir : & Path, sha : & str ) -> Result< BTreeMap< String, ( String, String) > > {
113
let text = git ( git_dir, & [ " ls-tree" , " -r" , " -z" , sha] ) ? ;
114
let mut out = BTreeMap:: new( ) ;
115
for entry in text.split ( ' \0 ' ) {
116
if entry.is_empty ( ) {
117
continue ;
118
}
119
let Some ( ( meta, path) ) = entry.split_once ( ' \t ' ) else { continue } ;
120
let m: Vec< & str > = meta.split ( ' ' ) .collect ( ) ;
121
if m.len ( ) = = 3 {
122
out.insert ( path.to_string ( ) , ( m[ 0 ] .to_string ( ) , m[ 2 ] .to_string ( ) ) ) ;
123
}
124
}
125
Ok ( out)
126
}
127
128
129
130
fn change_id_for ( sha : & str , message : & str ) -> ChangeId {
131
for line in message.lines ( ) .rev ( ) {
132
if let Some ( v) = line.strip_prefix ( " Change-Id:" ) {
133
if let Some ( id) = ChangeId:: from_hex( v.trim ( ) ) {
134
return id;
135
}
136
}
137
}
138
let h = blake3:: hash( sha.as_bytes ( ) ) ;
139
ChangeId( h.as_bytes ( ) [ .. 16 ] .try_into ( ) .unwrap ( ) )
140
}
141
30ea9c1e Andy McCall 1 month ago ↶
142
143
struct GitKnown {
144
145
imported : HashMap< String, ChangeHash> ,
146
147
exported : HashMap< String, ObjectRef> ,
148
}
149
c3069537 Andy McCall 1 month ago ↶
150
# [ derive ( Debug, Clone, Default, PartialEq, Eq ) ]
79ab8528 Andy McCall 1 month ago ↶
151
pub struct GitExportReport {
152
pub exported : usize ,
153
pub skipped : usize ,
154
pub branch : String,
155
pub heads : usize ,
156
}
157
158
fn git ( dir : & Path, args : & [& str ]) -> Result< String> {
159
let out = Command:: new( " git" ) .arg ( " -C" ) .arg ( dir) .args ( args) .output ( ) .map_err ( | e | RepoError:: Msg( format! ( " running git: {e} " ) ) ) ? ;
160
if ! out.status.success ( ) {
161
return fail ( format! ( " git {} failed: {} " , args.join ( " " ) , String :: from_utf8_lossy( & out.stderr) .trim ( ) ) ) ;
162
}
163
Ok ( String :: from_utf8_lossy( & out.stdout) .trim ( ) .to_string ( ) )
164
}
165
166
impl Repo {
167
fn git_marks_dir ( & self , git_dir : & Path) -> Result< PathBuf> {
168
let canonical = git_dir.canonicalize ( ) .unwrap_or_else ( | _| git_dir.to_path_buf ( ) ) ;
169
let key = hash_object ( ObjectType:: Chunk, canonical.to_string_lossy ( ) .as_bytes ( ) ) .short ( 16 ) ;
170
let dir = self .oxy.join ( " git" ) .join ( key) ;
171
fs:: create_dir_all( & dir) ? ;
172
Ok ( dir)
173
}
174
30ea9c1e Andy McCall 1 month ago ↶
175
176
177
fn read_git_states ( path : & Path) -> Result< BTreeMap< ObjectRef, String> > {
178
let mut out = BTreeMap:: new( ) ;
179
if let Ok ( text) = fs:: read_to_string( path) {
180
for line in text.lines ( ) {
181
if let Some ( ( h, sha) ) = line.split_once ( ' ' ) {
182
if let Some ( h) = ObjectRef:: from_hex( h) {
183
out.insert ( h, sha.trim ( ) .to_string ( ) ) ;
184
}
185
}
186
}
187
}
188
Ok ( out)
189
}
190
191
fn record_git_state ( & self , git_dir : & Path, state : & ObjectRef, sha : & str ) -> Result< ( ) > {
192
let path = self .git_marks_dir ( git_dir) ? .join ( " git-states" ) ;
193
let mut states = Self :: read_git_states( & path) ? ;
194
states.insert ( * state, sha.to_string ( ) ) ;
195
let text: String = states.iter ( ) .map ( | ( h , s ) | format! ( " {} {s} \n " , h.to_hex ( ) ) ) .collect ( ) ;
196
write_atomic ( & path, text.as_bytes ( ) )
197
}
198
199
200
fn git_mirror ( & self , url : & str ) -> Result< PathBuf> {
201
let key = hash_object ( ObjectType:: Chunk, url.as_bytes ( ) ) .short ( 16 ) ;
202
let dir = self .oxy.join ( " git" ) .join ( key) .join ( " mirror.git" ) ;
203
if ! dir.join ( " HEAD" ) .exists ( ) {
204
fs:: create_dir_all( & dir) ? ;
205
git ( & dir, & [ " init" , " --bare" , " -q" ] ) ? ;
206
}
207
Ok ( dir)
208
}
209
210
211
fn imported_commits ( & self ) -> Result< HashMap< String, ChangeHash> > {
212
let mut map = HashMap:: new( ) ;
213
for e in self .log ( ) ? {
214
if e.is_working_copy {
215
continue ;
216
}
217
if let Some ( sha) = e.change.message.lines ( ) .rev ( ) .find_map ( | l | l.strip_prefix ( " Git-Commit: " ) ) {
218
map.insert ( sha.trim ( ) .to_string ( ) , e.hash) ;
219
}
220
}
221
Ok ( map)
222
}
223
224
225
226
fn git_mirror_fetch ( & self , mirror : & Path, url : & str , branch : & str ) -> Result< ( ) > {
227
let spec = format! ( " +refs/heads/{branch} :refs/heads/{branch} " ) ;
228
match git ( mirror, & [ " fetch" , " -q" , url, & spec] ) {
229
Ok ( _ ) => Ok ( ( ) ) ,
230
Err ( e) => {
231
let ls = git ( mirror, & [ " ls-remote" , " --heads" , url, branch] ) .unwrap_or_default ( ) ;
232
if ls.trim ( ) .is_empty ( ) { Ok ( ( ) ) } else { Err ( e) }
233
}
234
}
235
}
236
237
238
239
240
241
pub fn git_fetch ( & self , url : & str , branch : & str ) -> Result< GitImportReport> {
242
let mirror = self .git_mirror ( url) ? ;
243
self .git_mirror_fetch ( & mirror, url, branch) ? ;
244
if git ( & mirror, & [ " rev-parse" , " --verify" , " -q" , & format! ( " refs/heads/{branch} " ) ] ) .is_err ( ) {
245
return fail ( format! ( " {url} has no branch {branch:?} " ) ) ;
246
}
247
self .import_git_commits ( & mirror, branch, & format! ( " oxy git fetch {url} {branch} " ) )
248
}
249
250
251
252
253
pub fn git_push ( & self , url : & str , branch : & str ) -> Result< GitExportReport> {
254
let mirror = self .git_mirror ( url) ? ;
255
self .git_mirror_fetch ( & mirror, url, branch) ? ;
256
if let Ok ( tip) = git ( & mirror, & [ " rev-parse" , " --verify" , " -q" , & format! ( " refs/heads/{branch} " ) ] ) {
257
let known = self .git_known ( & mirror) ? ;
258
if ! known.imported.contains_key ( & tip) & & ! known.exported.contains_key ( & tip) {
259
return fail ( format! ( " {url} {branch} has commits not yet fetched; run `oxy git fetch {url} ` first" ) ) ;
260
}
261
}
262
let report = self .git_export_as ( & mirror, branch) ? ;
263
let spec = format! ( " refs/heads/{branch} :refs/heads/{branch} " ) ;
264
git ( & mirror, & [ " push" , " -q" , url, & spec] ) ? ;
265
Ok ( report)
266
}
267
79ab8528 Andy McCall 1 month ago ↶
268
fn read_oxy_marks ( path : & Path) -> Result< BTreeMap< ObjectRef, u64 > > {
269
let mut out = BTreeMap:: new( ) ;
270
if let Ok ( text) = fs:: read_to_string( path) {
271
for line in text.lines ( ) {
272
if let Some ( ( h, m) ) = line.split_once ( ' ' ) {
273
if let ( Some ( h) , Ok ( m) ) = ( ObjectRef:: from_hex( h) , m.parse ( ) ) {
274
out.insert ( h, m) ;
275
}
276
}
277
}
278
}
279
Ok ( out)
280
}
281
282
283
284
285
286
pub fn git_export ( & self , git_dir : & Path) -> Result< GitExportReport> {
30ea9c1e Andy McCall 1 month ago ↶
287
let bookmark = self .config.bookmark.clone ( ) ;
288
self .git_export_as ( git_dir, & bookmark)
289
}
290
291
292
293
294
pub fn git_export_as ( & self , git_dir : & Path, branch : & str ) -> Result< GitExportReport> {
79ab8528 Andy McCall 1 month ago ↶
295
if ! git_dir.join ( " HEAD" ) .exists ( ) & & ! git_dir.join ( " .git" ) .exists ( ) {
296
fs:: create_dir_all( git_dir) ? ;
297
git ( git_dir, & [ " init" , " --bare" , " -q" ] ) ? ;
298
}
299
let marks_dir = self .git_marks_dir ( git_dir) ? ;
300
let git_marks = marks_dir.join ( " git-marks" ) ;
301
let oxy_marks_path = marks_dir.join ( " oxy-marks" ) ;
302
let mut marks = Self :: read_oxy_marks( & oxy_marks_path) ? ;
30ea9c1e Andy McCall 1 month ago ↶
303
let git_states = Self :: read_git_states( & marks_dir.join ( " git-states" ) ) ? ;
79ab8528 Andy McCall 1 month ago ↶
304
let mut next_mark = marks.values ( ) .copied ( ) .max ( ) .unwrap_or ( 0 ) + 1 ;
305
306
let bookmark = self .config.bookmark.clone ( ) ;
307
let mut stream: Vec< u8 > = Vec :: new( ) ;
30ea9c1e Andy McCall 1 month ago ↶
308
let mut report = GitExportReport { branch: branch.to_string ( ) , heads: self .bookmark_heads ( ) ? .len ( ) , .. Default :: default( ) } ;
79ab8528 Andy McCall 1 month ago ↶
309
310
311
let mut ops = self .op_log ( ) ? ;
312
ops.reverse ( ) ;
313
let mut prev: Option< ( ObjectRef, Vec< ChangeHash> ) > = None ;
30ea9c1e Andy McCall 1 month ago ↶
314
315
let mut last_parent: Option< String> = None ;
79ab8528 Andy McCall 1 month ago ↶
316
for ( _ , op) in & ops {
317
let Some ( state_ref) = op.view.bookmarks.get ( & bookmark) .copied ( ) else { continue } ;
318
if prev.as_ref ( ) .is_some_and ( | ( p , _) | * p = = state_ref) {
319
continue ;
320
}
321
let Ok ( state) = self .store.get_state ( & state_ref) else { continue } ;
322
if state.heads.is_empty ( ) {
323
prev = Some ( ( state_ref, vec! [ ] ) ) ;
324
continue ;
325
}
30ea9c1e Andy McCall 1 month ago ↶
326
if let Some ( sha) = git_states.get ( & state_ref) {
327
report.skipped + = 1 ;
328
last_parent = Some ( sha.clone ( ) ) ;
329
prev = Some ( ( state_ref, state.heads.clone ( ) ) ) ;
330
continue ;
331
}
79ab8528 Andy McCall 1 month ago ↶
332
if let Some ( m) = marks.get ( & state_ref) {
333
report.skipped + = 1 ;
30ea9c1e Andy McCall 1 month ago ↶
334
last_parent = Some ( format! ( " :{m} " ) ) ;
79ab8528 Andy McCall 1 month ago ↶
335
prev = Some ( ( state_ref, state.heads.clone ( ) ) ) ;
336
continue ;
337
}
338
let prev_heads = prev.as_ref ( ) .map ( | ( _, h ) | h.clone ( ) ) .unwrap_or_default ( ) ;
339
let added = self .negotiate ( & prev_heads, & state.heads) .unwrap_or_default ( ) ;
340
let loaded = self .load_state ( & state.heads) ? ;
341
let snapshot = loaded.materialize ( ) ;
342
let ( author, secs, mut message) = match added.as_slice ( ) {
343
[ h] => {
344
let c = self .store.get_change ( h) ? ;
345
let mut m = c.message.clone ( ) ;
346
if ! m.ends_with ( ' \n ' ) {
347
m.push ( ' \n ' ) ;
348
}
349
m.push_str ( & format! ( " \n Change-Id: {} \n Oxy-Hash: {} \n " , c.change_id.to_hex ( ) , h.to_hex ( ) ) ) ;
350
( self .git_author ( & c) , c.hlc.wall_ms / 1000 , m)
351
}
352
[ ] => ( " oxy <oxy@localhost>" .to_string ( ) , op.hlc.wall_ms / 1000 , format! ( " oxy: {} \n \n " , op.command) ) ,
353
many => {
354
let mut m = format! ( " oxy: {} ({} changes)\n \n " , op.command, many.len ( ) ) ;
355
for h in many {
356
let c = self .store.get_change ( h) ? ;
357
m.push_str ( & format! ( " - {} {} \n " , c.change_id.short ( 8 ) , c.message.lines ( ) .next ( ) .unwrap_or ( " " ) ) ) ;
358
}
359
m.push_str ( & format! ( " \n Oxy-Changes: {} \n " , many.iter ( ) .map ( | h | h.to_hex ( ) ) .collect:: < Vec< _ > > ( ) .join ( " " ) ) ) ;
360
( " oxy <oxy@localhost>" .to_string ( ) , op.hlc.wall_ms / 1000 , m)
361
}
362
} ;
363
message.push_str ( & format! ( " Oxy-State: {} \n " , state_ref.to_hex ( ) ) ) ;
364
if snapshot.is_conflicted ( ) {
365
message.push_str ( " Oxy-Conflicted: true\n " ) ;
366
}
367
let mark = next_mark;
368
next_mark + = 1 ;
30ea9c1e Andy McCall 1 month ago ↶
369
let parents: Vec< String> = last_parent.clone ( ) .into_iter ( ) .collect ( ) ;
79ab8528 Andy McCall 1 month ago ↶
370
self .write_commit ( & mut stream, " refs/oxy/export" , mark, & author, secs, & message, & parents, & snapshot) ? ;
371
marks.insert ( state_ref, mark) ;
30ea9c1e Andy McCall 1 month ago ↶
372
last_parent = Some ( format! ( " :{mark} " ) ) ;
79ab8528 Andy McCall 1 month ago ↶
373
prev = Some ( ( state_ref, state.heads.clone ( ) ) ) ;
374
report.exported + = 1 ;
375
}
30ea9c1e Andy McCall 1 month ago ↶
376
if let Some ( p) = & last_parent {
377
stream.extend_from_slice ( format! ( " reset refs/heads/{branch} \n from {p} \n \n " ) .as_bytes ( ) ) ;
79ab8528 Andy McCall 1 month ago ↶
378
}
132b884b Andy McCall 1 month ago ↶
379
380
381
for t in self .tags ( ) ? {
30ea9c1e Andy McCall 1 month ago ↶
382
let target = match ( git_states.get ( & t.state) , marks.get ( & t.state) ) {
383
( Some ( sha) , _ ) => sha.clone ( ) ,
384
( None , Some ( m) ) => format! ( " :{m} " ) ,
385
( None , None ) => {
132b884b Andy McCall 1 month ago ↶
386
let loaded = self .load_state ( & t.heads) ? ;
387
let snapshot = loaded.materialize ( ) ;
388
let mark = next_mark;
389
next_mark + = 1 ;
390
let message = format! ( " oxy: tag {} \n \n {} Oxy-State: {} \n " , t.name, if t.message.is_empty ( ) { String :: new( ) } else { format! ( " {} \n \n " , t.message) } , t.state.to_hex ( ) ) ;
391
self .write_commit ( & mut stream, " refs/oxy/export" , mark, " oxy <oxy@localhost>" , Hlc:: now( ) .wall_ms / 1000 , & message, & [ ] , & snapshot) ? ;
392
marks.insert ( t.state, mark) ;
393
report.exported + = 1 ;
30ea9c1e Andy McCall 1 month ago ↶
394
format! ( " :{mark} " )
132b884b Andy McCall 1 month ago ↶
395
}
396
} ;
30ea9c1e Andy McCall 1 month ago ↶
397
stream.extend_from_slice ( format! ( " reset refs/tags/{} \n from {target} \n \n " , t.name) .as_bytes ( ) ) ;
132b884b Andy McCall 1 month ago ↶
398
}
399
stream.extend_from_slice ( b " done\n " ) ;
79ab8528 Andy McCall 1 month ago ↶
400
401
let mut cmd = Command:: new( " git" ) ;
402
403
cmd.arg ( " -C" ) .arg ( git_dir) .args ( [ " fast-import" , " --quiet" , " --done" , " --force" ] ) .arg ( format! ( " --export-marks={} " , git_marks.display ( ) ) ) ;
404
if git_marks.exists ( ) {
405
cmd.arg ( format! ( " --import-marks={} " , git_marks.display ( ) ) ) ;
406
}
407
let mut child = cmd.stdin ( Stdio:: piped( ) ) .stdout ( Stdio:: null( ) ) .stderr ( Stdio:: piped( ) ) .spawn ( ) .map_err ( | e | RepoError:: Msg( format! ( " running git fast-import: {e} " ) ) ) ? ;
408
child.stdin.take ( ) .unwrap ( ) .write_all ( & stream) ? ;
409
let out = child.wait_with_output ( ) ? ;
410
if ! out.status.success ( ) {
411
return fail ( format! ( " git fast-import failed: {} " , String :: from_utf8_lossy( & out.stderr) .trim ( ) ) ) ;
412
}
413
let text: String = marks.iter ( ) .map ( | ( h , m ) | format! ( " {} {m} \n " , h.to_hex ( ) ) ) .collect ( ) ;
414
write_atomic ( & oxy_marks_path, text.as_bytes ( ) ) ? ;
415
let _ = git ( git_dir, & [ " update-ref" , " -d" , " refs/oxy/export" ] ) ;
416
Ok ( report)
417
}
418
c3069537 Andy McCall 1 month ago ↶
419
420
421
422
423
pub fn import_git ( & self , git_dir : & Path, branch : & str ) -> Result< GitImportReport> {
424
if ! self .bookmark_heads ( ) ? .is_empty ( ) {
425
return fail ( " git import needs an empty bookmark; import into a fresh repository" ) ;
426
}
30ea9c1e Andy McCall 1 month ago ↶
427
self .import_git_commits ( git_dir, branch, & format! ( " oxy git import {} {branch} " , git_dir.display ( ) ) )
428
}
429
430
431
432
fn import_git_commits ( & self , git_dir : & Path, branch : & str , command : & str ) -> Result< GitImportReport> {
8f10a07a Andy McCall 1 month ago ↶
433
let batch = self .store.begin_batch ( ) ? ;
434
let result = self .import_git_commits_batched ( git_dir, branch, command) ;
435
batch.finish ( ) ? ;
436
result
437
}
438
439
fn import_git_commits_batched ( & self , git_dir : & Path, branch : & str , command : & str ) -> Result< GitImportReport> {
c3069537 Andy McCall 1 month ago ↶
440
let commits = list_commits ( git_dir, branch) ? ;
441
let mut cat = CatFile:: start( git_dir) ? ;
442
let mut report = GitImportReport:: default( ) ;
30ea9c1e Andy McCall 1 month ago ↶
443
let mut known = self .git_known ( git_dir) ? ;
444
let old_heads = self .bookmark_heads ( ) ? ;
445
let mut new_changes: Vec< Change> = Vec :: new( ) ;
c3069537 Andy McCall 1 month ago ↶
446
447
448
let mut last: Option< ( String, State) > = None ;
449
let mut blob_cache: HashMap< String, Vec< u8 > > = HashMap:: new( ) ;
450
let mut cache_bytes: usize = 0 ;
451
452
for gc in & commits {
30ea9c1e Andy McCall 1 month ago ↶
453
if known.imported.contains_key ( & gc.sha) | | known.exported.contains_key ( & gc.sha) {
454
continue ;
455
}
c3069537 Andy McCall 1 month ago ↶
456
457
let mut base = match ( & gc.parents.first ( ) , & last) {
458
( Some ( p1) , Some ( ( sha, st) ) ) if * p1 = = sha => st.clone ( ) ,
30ea9c1e Andy McCall 1 month ago ↶
459
( Some ( p1) , _ ) => {
460
let mut st = State:: new( ) ;
461
self .add_git_ancestors ( & mut st, git_dir, & [ p1.as_str ( ) ] , & known) ? ;
462
st
463
}
c3069537 Andy McCall 1 month ago ↶
464
( None , _ ) => State:: new( ) ,
465
} ;
466
if gc.parents.len ( ) > 1 {
467
report.merges + = 1 ;
468
let mut args: Vec< String> = gc.parents[ 1 .. ] .to_vec ( ) ;
469
args.push ( format! ( " ^{} " , gc.parents[ 0 ] ) ) ;
30ea9c1e Andy McCall 1 month ago ↶
470
let refs: Vec< & str > = args.iter ( ) .map ( String :: as_str) .collect ( ) ;
471
self .add_git_ancestors ( & mut base, git_dir, & refs, & known) ? ;
c3069537 Andy McCall 1 month ago ↶
472
}
473
let base_snapshot = base.materialize ( ) ;
474
475
476
let mut entries: BTreeMap< String, FsEntry> = BTreeMap:: new( ) ;
477
for ( path, ( mode, sha) ) in list_tree ( git_dir, & gc.sha) ? {
478
let ( kind, exec) = match mode.as_str ( ) {
479
" 100644" | " 100664" => ( NodeKind:: File, false ) ,
480
" 100755" => ( NodeKind:: File, true ) ,
481
" 120000" => ( NodeKind:: Symlink, false ) ,
482
" 160000" => {
483
report.skipped_submodules + = 1 ;
484
continue ;
485
}
486
_ => continue ,
487
} ;
488
let content = match blob_cache.get ( & sha) {
489
Some ( c) => c.clone ( ) ,
490
None => {
491
let c = cat.read ( & sha) ? ;
492
if cache_bytes + c.len ( ) > 256 * 1024 * 1024 {
493
blob_cache.clear ( ) ;
494
cache_bytes = 0 ;
495
}
496
cache_bytes + = c.len ( ) ;
497
blob_cache.insert ( sha.clone ( ) , c.clone ( ) ) ;
498
c
499
}
500
} ;
501
entries.insert ( path, FsEntry { kind, exec, content } ) ;
502
}
503
504
let built = self .build_from_entries ( & base, & base_snapshot, HashMap:: new( ) , & entries) ? ;
505
let mut deps: BTreeSet< ChangeHash> = built.contributors.iter ( ) .copied ( ) .collect ( ) ;
506
let mut message = gc.message.clone ( ) ;
507
if ! message.ends_with ( ' \n ' ) {
508
message.push ( ' \n ' ) ;
509
}
510
message.push_str ( & format! ( " \n Git-Commit: {} \n Git-Author: {} \n " , gc.sha, gc.author) ) ;
511
let mut change = Change {
512
change_id: change_id_for ( & gc.sha, & gc.message) ,
513
deps: vec! [ ] ,
514
author: ObjectRef:: ZERO ,
515
hlc: Hlc { wall_ms: gc.author_secs * 1000 , logical: 0 } ,
516
message,
517
tree_ops: built.tree_ops,
518
edits: built.edits,
519
kind: if built.resolves { ChangeKind:: Resolution } else { ChangeKind:: Normal } ,
520
} ;
521
deps.extend ( referenced_changes ( & change, & base) ) ;
522
change.deps = deps.into_iter ( ) .collect ( ) ;
523
change.normalize ( ) ;
524
let hash = self .store.put_change ( & change) ? ;
30ea9c1e Andy McCall 1 month ago ↶
525
new_changes.push ( change.clone ( ) ) ;
c3069537 Andy McCall 1 month ago ↶
526
base.add_unchecked ( change) ;
30ea9c1e Andy McCall 1 month ago ↶
527
known.imported.insert ( gc.sha.clone ( ) , hash) ;
c3069537 Andy McCall 1 month ago ↶
528
last = Some ( ( gc.sha.clone ( ) , base) ) ;
529
report.commits + = 1 ;
530
}
531
30ea9c1e Andy McCall 1 month ago ↶
532
533
report.heads = old_heads.len ( ) ;
534
if last.is_none ( ) {
535
return Ok ( report) ;
536
}
537
let mut all = self .load_state ( & old_heads) ? ;
538
for c in new_changes {
539
all.add_unchecked ( c) ;
540
}
541
let mut heads = all.heads ( ) ;
542
heads.sort ( ) ;
c3069537 Andy McCall 1 month ago ↶
543
report.heads = heads.len ( ) ;
544
let ( op_ref, op) = self .head_op ( ) ? ;
545
let mut view = op.view.clone ( ) ;
30ea9c1e Andy McCall 1 month ago ↶
546
let old_state_ref = view.bookmarks.get ( & self .config.bookmark) .copied ( ) ;
547
let state_ref = self .store.put_state ( & StateObject:: new( heads.clone ( ) ) ) ? ;
548
view.bookmarks.insert ( self .config.bookmark.clone ( ) , state_ref) ;
549
self .record_op ( & [ op_ref] , command, view) ? ;
550
551
552
let old_known = old_heads.is_empty ( ) | | old_state_ref.is_some_and ( | s | known.exported.values ( ) .any ( | e | * e = = s) ) ;
553
if let ( Some ( tip) , true ) = ( commits.last ( ) , old_known) {
554
self .record_git_state ( git_dir, & state_ref, & tip.sha) ? ;
555
}
556
557
if old_heads.is_empty ( ) {
558
self .checkout ( & heads, true , & format! ( " {command} (checkout)" ) ) ? ;
559
} else {
560
let snap = self .snapshot ( command) ? ;
561
let clean = snap.wc.tree_ops.is_empty ( ) & & snap.wc.edits.is_empty ( ) ;
562
let mut base = snap.base.maximal ( & snap.wc.deps) ;
563
base.sort ( ) ;
564
let mut sorted_old = old_heads.clone ( ) ;
565
sorted_old.sort ( ) ;
566
if clean & & base = = sorted_old {
567
self .checkout ( & heads, false , & format! ( " {command} (checkout)" ) ) ? ;
568
}
569
}
c3069537 Andy McCall 1 month ago ↶
570
Ok ( report)
571
}
572
30ea9c1e Andy McCall 1 month ago ↶
573
574
575
576
fn add_git_ancestors ( & self , st : & mut State, git_dir : & Path, args : & [& str ], known : & GitKnown) -> Result< ( ) > {
577
let mut full = vec! [ " rev-list" ] ;
578
full.extend ( args) ;
579
let out = git ( git_dir, & full) ? ;
c3069537 Andy McCall 1 month ago ↶
580
for sha in out.lines ( ) {
30ea9c1e Andy McCall 1 month ago ↶
581
if let Some ( h) = known.imported.get ( sha) {
c3069537 Andy McCall 1 month ago ↶
582
st.add_unchecked ( self .store.get_change ( h) ? ) ;
30ea9c1e Andy McCall 1 month ago ↶
583
} else if let Some ( state_ref) = known.exported.get ( sha) {
584
let heads = self .store.get_state ( state_ref) ? .heads;
585
for h in self .negotiate ( & [ ] , & heads) ? {
586
st.add_unchecked ( self .store.get_change ( & h) ? ) ;
587
}
588
}
589
}
590
Ok ( ( ) )
591
}
592
593
594
595
fn git_known ( & self , git_dir : & Path) -> Result< GitKnown> {
596
let imported = self .imported_commits ( ) ? ;
597
let marks_dir = self .git_marks_dir ( git_dir) ? ;
598
let oxy_marks = Self :: read_oxy_marks( & marks_dir.join ( " oxy-marks" ) ) ? ;
599
let mut sha_of_mark: HashMap< u64 , String> = HashMap:: new( ) ;
600
if let Ok ( text) = fs:: read_to_string( marks_dir.join ( " git-marks" ) ) {
601
for line in text.lines ( ) {
602
if let Some ( ( m, sha) ) = line.split_once ( ' ' ) {
603
if let Ok ( m) = m.trim_start_matches ( ' :' ) .parse:: < u64 > ( ) {
604
sha_of_mark.insert ( m, sha.trim ( ) .to_string ( ) ) ;
605
}
606
}
607
}
608
}
609
let mut exported = HashMap:: new( ) ;
610
for ( state_ref, m) in oxy_marks {
611
if let Some ( sha) = sha_of_mark.get ( & m) {
612
exported.insert ( sha.clone ( ) , state_ref) ;
c3069537 Andy McCall 1 month ago ↶
613
}
614
}
30ea9c1e Andy McCall 1 month ago ↶
615
for ( state_ref, sha) in Self :: read_git_states( & marks_dir.join ( " git-states" ) ) ? {
616
exported.entry ( sha) .or_insert ( state_ref) ;
617
}
618
Ok ( GitKnown { imported, exported } )
c3069537 Andy McCall 1 month ago ↶
619
}
620
79ab8528 Andy McCall 1 month ago ↶
621
fn git_author ( & self , c : & Change) -> String {
622
match self .store.get_identity ( & c.author) {
623
Ok ( id) if ! id.name.is_empty ( ) => format! ( " {} <{} >" , id.name.replace ( [ ' <' , ' >' , ' \n ' ] , " " ) , id.email.replace ( [ ' <' , ' >' , ' \n ' , ' ' ] , " " ) ) ,
624
_ => " oxy <oxy@localhost>" .to_string ( ) ,
625
}
626
}
627
628
# [ allow ( clippy::too_many_arguments ) ]
30ea9c1e Andy McCall 1 month ago ↶
629
fn write_commit ( & self , out : & mut Vec< u8 > , reference : & str , mark : u64 , author : & str , secs : u64 , message : & str , parents : & [String], snapshot : & Snapshot) -> Result< ( ) > {
79ab8528 Andy McCall 1 month ago ↶
630
out.extend_from_slice ( format! ( " commit {reference} \n mark :{mark} \n author {author} {secs} +0000\n committer {author} {secs} +0000\n data {} \n {message} \n " , message.len ( ) ) .as_bytes ( ) ) ;
631
for ( i, p) in parents.iter ( ) .enumerate ( ) {
30ea9c1e Andy McCall 1 month ago ↶
632
out.extend_from_slice ( format! ( " {} {p} \n " , if i = = 0 { " from" } else { " merge" } ) .as_bytes ( ) ) ;
79ab8528 Andy McCall 1 month ago ↶
633
}
634
out.extend_from_slice ( b " deleteall\n " ) ;
635
for ( path, f) in & snapshot.files {
636
let ( mode, bytes) : ( & str , Vec< u8 > ) = match ( & f.kind, & f.content) {
637
( NodeKind:: Dir, _ ) => continue ,
638
( NodeKind:: Symlink, FileContent:: Opaque { blobs } ) => ( " 120000" , blobs.first ( ) .map ( | b | self .read_blob ( b) ) .transpose ( ) ? .unwrap_or_default ( ) ) ,
639
( _ , FileContent:: Text { bytes, .. } ) => ( if f.mode.exec { " 100755" } else { " 100644" } , bytes.clone ( ) ) ,
640
( _ , FileContent:: Opaque { blobs } ) => ( if f.mode.exec { " 100755" } else { " 100644" } , blobs.first ( ) .map ( | b | self .read_blob ( b) ) .transpose ( ) ? .unwrap_or_default ( ) ) ,
641
( _ , FileContent:: Dir) => continue ,
642
} ;
643
out.extend_from_slice ( format! ( " M {mode} inline {path} \n data {} \n " , bytes.len ( ) ) .as_bytes ( ) ) ;
644
out.extend_from_slice ( & bytes) ;
645
out.push ( b ' \n ' ) ;
646
}
647
out.push ( b ' \n ' ) ;
648
Ok ( ( ) )
649
}
650
}