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
7891e21c Andy McCall 1 month ago ↶
211
pub ( crate ) fn imported_commits ( & self ) -> Result< HashMap< String, ChangeHash> > {
30ea9c1e Andy McCall 1 month ago ↶
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> {
c3069537 Andy McCall 1 month ago ↶
433
let commits = list_commits ( git_dir, branch) ? ;
434
let mut cat = CatFile:: start( git_dir) ? ;
435
let mut report = GitImportReport:: default( ) ;
30ea9c1e Andy McCall 1 month ago ↶
436
let mut known = self .git_known ( git_dir) ? ;
437
let old_heads = self .bookmark_heads ( ) ? ;
438
let mut new_changes: Vec< Change> = Vec :: new( ) ;
c3069537 Andy McCall 1 month ago ↶
439
440
441
let mut last: Option< ( String, State) > = None ;
442
let mut blob_cache: HashMap< String, Vec< u8 > > = HashMap:: new( ) ;
443
let mut cache_bytes: usize = 0 ;
444
445
for gc in & commits {
30ea9c1e Andy McCall 1 month ago ↶
446
if known.imported.contains_key ( & gc.sha) | | known.exported.contains_key ( & gc.sha) {
447
continue ;
448
}
c3069537 Andy McCall 1 month ago ↶
449
450
let mut base = match ( & gc.parents.first ( ) , & last) {
451
( Some ( p1) , Some ( ( sha, st) ) ) if * p1 = = sha => st.clone ( ) ,
30ea9c1e Andy McCall 1 month ago ↶
452
( Some ( p1) , _ ) => {
453
let mut st = State:: new( ) ;
454
self .add_git_ancestors ( & mut st, git_dir, & [ p1.as_str ( ) ] , & known) ? ;
455
st
456
}
c3069537 Andy McCall 1 month ago ↶
457
( None , _ ) => State:: new( ) ,
458
} ;
459
if gc.parents.len ( ) > 1 {
460
report.merges + = 1 ;
461
let mut args: Vec< String> = gc.parents[ 1 .. ] .to_vec ( ) ;
462
args.push ( format! ( " ^{} " , gc.parents[ 0 ] ) ) ;
30ea9c1e Andy McCall 1 month ago ↶
463
let refs: Vec< & str > = args.iter ( ) .map ( String :: as_str) .collect ( ) ;
464
self .add_git_ancestors ( & mut base, git_dir, & refs, & known) ? ;
c3069537 Andy McCall 1 month ago ↶
465
}
466
let base_snapshot = base.materialize ( ) ;
467
468
469
let mut entries: BTreeMap< String, FsEntry> = BTreeMap:: new( ) ;
470
for ( path, ( mode, sha) ) in list_tree ( git_dir, & gc.sha) ? {
471
let ( kind, exec) = match mode.as_str ( ) {
472
" 100644" | " 100664" => ( NodeKind:: File, false ) ,
473
" 100755" => ( NodeKind:: File, true ) ,
474
" 120000" => ( NodeKind:: Symlink, false ) ,
475
" 160000" => {
476
report.skipped_submodules + = 1 ;
477
continue ;
478
}
479
_ => continue ,
480
} ;
481
let content = match blob_cache.get ( & sha) {
482
Some ( c) => c.clone ( ) ,
483
None => {
484
let c = cat.read ( & sha) ? ;
485
if cache_bytes + c.len ( ) > 256 * 1024 * 1024 {
486
blob_cache.clear ( ) ;
487
cache_bytes = 0 ;
488
}
489
cache_bytes + = c.len ( ) ;
490
blob_cache.insert ( sha.clone ( ) , c.clone ( ) ) ;
491
c
492
}
493
} ;
494
entries.insert ( path, FsEntry { kind, exec, content } ) ;
495
}
496
497
let built = self .build_from_entries ( & base, & base_snapshot, HashMap:: new( ) , & entries) ? ;
498
let mut deps: BTreeSet< ChangeHash> = built.contributors.iter ( ) .copied ( ) .collect ( ) ;
499
let mut message = gc.message.clone ( ) ;
500
if ! message.ends_with ( ' \n ' ) {
501
message.push ( ' \n ' ) ;
502
}
503
message.push_str ( & format! ( " \n Git-Commit: {} \n Git-Author: {} \n " , gc.sha, gc.author) ) ;
504
let mut change = Change {
505
change_id: change_id_for ( & gc.sha, & gc.message) ,
506
deps: vec! [ ] ,
507
author: ObjectRef:: ZERO ,
508
hlc: Hlc { wall_ms: gc.author_secs * 1000 , logical: 0 } ,
509
message,
510
tree_ops: built.tree_ops,
511
edits: built.edits,
512
kind: if built.resolves { ChangeKind:: Resolution } else { ChangeKind:: Normal } ,
513
} ;
514
deps.extend ( referenced_changes ( & change, & base) ) ;
515
change.deps = deps.into_iter ( ) .collect ( ) ;
516
change.normalize ( ) ;
517
let hash = self .store.put_change ( & change) ? ;
30ea9c1e Andy McCall 1 month ago ↶
518
new_changes.push ( change.clone ( ) ) ;
c3069537 Andy McCall 1 month ago ↶
519
base.add_unchecked ( change) ;
30ea9c1e Andy McCall 1 month ago ↶
520
known.imported.insert ( gc.sha.clone ( ) , hash) ;
c3069537 Andy McCall 1 month ago ↶
521
last = Some ( ( gc.sha.clone ( ) , base) ) ;
522
report.commits + = 1 ;
523
}
524
30ea9c1e Andy McCall 1 month ago ↶
525
526
report.heads = old_heads.len ( ) ;
527
if last.is_none ( ) {
528
return Ok ( report) ;
529
}
530
let mut all = self .load_state ( & old_heads) ? ;
531
for c in new_changes {
532
all.add_unchecked ( c) ;
533
}
534
let mut heads = all.heads ( ) ;
535
heads.sort ( ) ;
c3069537 Andy McCall 1 month ago ↶
536
report.heads = heads.len ( ) ;
537
let ( op_ref, op) = self .head_op ( ) ? ;
538
let mut view = op.view.clone ( ) ;
30ea9c1e Andy McCall 1 month ago ↶
539
let old_state_ref = view.bookmarks.get ( & self .config.bookmark) .copied ( ) ;
540
let state_ref = self .store.put_state ( & StateObject:: new( heads.clone ( ) ) ) ? ;
541
view.bookmarks.insert ( self .config.bookmark.clone ( ) , state_ref) ;
542
self .record_op ( & [ op_ref] , command, view) ? ;
543
544
545
let old_known = old_heads.is_empty ( ) | | old_state_ref.is_some_and ( | s | known.exported.values ( ) .any ( | e | * e = = s) ) ;
546
if let ( Some ( tip) , true ) = ( commits.last ( ) , old_known) {
547
self .record_git_state ( git_dir, & state_ref, & tip.sha) ? ;
548
}
549
550
if old_heads.is_empty ( ) {
551
self .checkout ( & heads, true , & format! ( " {command} (checkout)" ) ) ? ;
552
} else {
553
let snap = self .snapshot ( command) ? ;
554
let clean = snap.wc.tree_ops.is_empty ( ) & & snap.wc.edits.is_empty ( ) ;
555
let mut base = snap.base.maximal ( & snap.wc.deps) ;
556
base.sort ( ) ;
557
let mut sorted_old = old_heads.clone ( ) ;
558
sorted_old.sort ( ) ;
559
if clean & & base = = sorted_old {
560
self .checkout ( & heads, false , & format! ( " {command} (checkout)" ) ) ? ;
561
}
562
}
c3069537 Andy McCall 1 month ago ↶
563
Ok ( report)
564
}
565
30ea9c1e Andy McCall 1 month ago ↶
566
567
568
569
fn add_git_ancestors ( & self , st : & mut State, git_dir : & Path, args : & [& str ], known : & GitKnown) -> Result< ( ) > {
570
let mut full = vec! [ " rev-list" ] ;
571
full.extend ( args) ;
572
let out = git ( git_dir, & full) ? ;
c3069537 Andy McCall 1 month ago ↶
573
for sha in out.lines ( ) {
30ea9c1e Andy McCall 1 month ago ↶
574
if let Some ( h) = known.imported.get ( sha) {
c3069537 Andy McCall 1 month ago ↶
575
st.add_unchecked ( self .store.get_change ( h) ? ) ;
30ea9c1e Andy McCall 1 month ago ↶
576
} else if let Some ( state_ref) = known.exported.get ( sha) {
577
let heads = self .store.get_state ( state_ref) ? .heads;
578
for h in self .negotiate ( & [ ] , & heads) ? {
579
st.add_unchecked ( self .store.get_change ( & h) ? ) ;
580
}
581
}
582
}
583
Ok ( ( ) )
584
}
585
586
587
588
fn git_known ( & self , git_dir : & Path) -> Result< GitKnown> {
589
let imported = self .imported_commits ( ) ? ;
590
let marks_dir = self .git_marks_dir ( git_dir) ? ;
591
let oxy_marks = Self :: read_oxy_marks( & marks_dir.join ( " oxy-marks" ) ) ? ;
592
let mut sha_of_mark: HashMap< u64 , String> = HashMap:: new( ) ;
593
if let Ok ( text) = fs:: read_to_string( marks_dir.join ( " git-marks" ) ) {
594
for line in text.lines ( ) {
595
if let Some ( ( m, sha) ) = line.split_once ( ' ' ) {
596
if let Ok ( m) = m.trim_start_matches ( ' :' ) .parse:: < u64 > ( ) {
597
sha_of_mark.insert ( m, sha.trim ( ) .to_string ( ) ) ;
598
}
599
}
600
}
601
}
602
let mut exported = HashMap:: new( ) ;
603
for ( state_ref, m) in oxy_marks {
604
if let Some ( sha) = sha_of_mark.get ( & m) {
605
exported.insert ( sha.clone ( ) , state_ref) ;
c3069537 Andy McCall 1 month ago ↶
606
}
607
}
30ea9c1e Andy McCall 1 month ago ↶
608
for ( state_ref, sha) in Self :: read_git_states( & marks_dir.join ( " git-states" ) ) ? {
609
exported.entry ( sha) .or_insert ( state_ref) ;
610
}
611
Ok ( GitKnown { imported, exported } )
c3069537 Andy McCall 1 month ago ↶
612
}
613
79ab8528 Andy McCall 1 month ago ↶
614
fn git_author ( & self , c : & Change) -> String {
615
match self .store.get_identity ( & c.author) {
616
Ok ( id) if ! id.name.is_empty ( ) => format! ( " {} <{} >" , id.name.replace ( [ ' <' , ' >' , ' \n ' ] , " " ) , id.email.replace ( [ ' <' , ' >' , ' \n ' , ' ' ] , " " ) ) ,
617
_ => " oxy <oxy@localhost>" .to_string ( ) ,
618
}
619
}
620
621
# [ allow ( clippy::too_many_arguments ) ]
30ea9c1e Andy McCall 1 month ago ↶
622
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 ↶
623
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 ( ) ) ;
624
for ( i, p) in parents.iter ( ) .enumerate ( ) {
30ea9c1e Andy McCall 1 month ago ↶
625
out.extend_from_slice ( format! ( " {} {p} \n " , if i = = 0 { " from" } else { " merge" } ) .as_bytes ( ) ) ;
79ab8528 Andy McCall 1 month ago ↶
626
}
627
out.extend_from_slice ( b " deleteall\n " ) ;
628
for ( path, f) in & snapshot.files {
629
let ( mode, bytes) : ( & str , Vec< u8 > ) = match ( & f.kind, & f.content) {
630
( NodeKind:: Dir, _ ) => continue ,
631
( NodeKind:: Symlink, FileContent:: Opaque { blobs } ) => ( " 120000" , blobs.first ( ) .map ( | b | self .read_blob ( b) ) .transpose ( ) ? .unwrap_or_default ( ) ) ,
632
( _ , FileContent:: Text { bytes, .. } ) => ( if f.mode.exec { " 100755" } else { " 100644" } , bytes.clone ( ) ) ,
633
( _ , FileContent:: Opaque { blobs } ) => ( if f.mode.exec { " 100755" } else { " 100644" } , blobs.first ( ) .map ( | b | self .read_blob ( b) ) .transpose ( ) ? .unwrap_or_default ( ) ) ,
634
( _ , FileContent:: Dir) => continue ,
635
} ;
636
out.extend_from_slice ( format! ( " M {mode} inline {path} \n data {} \n " , bytes.len ( ) ) .as_bytes ( ) ) ;
637
out.extend_from_slice ( & bytes) ;
638
out.push ( b ' \n ' ) ;
639
}
640
out.push ( b ' \n ' ) ;
641
Ok ( ( ) )
642
}
643
}