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