@@ -193,6 +193,96 @@ impl GitMetadataBackend {
193193 }
194194}
195195
196+ #[ cfg( test) ]
197+ mod tests {
198+ use super :: * ;
199+ use std:: io:: Write ;
200+ use std:: process:: { Command , Stdio } ;
201+ use tempfile:: TempDir ;
202+
203+ fn run_git ( repo_path : & Path , args : & [ & str ] ) -> String {
204+ let output = Command :: new ( "git" )
205+ . args ( args)
206+ . current_dir ( repo_path)
207+ . output ( )
208+ . expect ( "failed to run git" ) ;
209+ assert ! (
210+ output. status. success( ) ,
211+ "git {:?} failed: stdout={} stderr={}" ,
212+ args,
213+ String :: from_utf8_lossy( & output. stdout) ,
214+ String :: from_utf8_lossy( & output. stderr)
215+ ) ;
216+ String :: from_utf8 ( output. stdout ) . expect ( "git output should be UTF-8" )
217+ }
218+
219+ fn write_commit_object ( repo_path : & Path , commit_content : & str ) -> String {
220+ let mut child = Command :: new ( "git" )
221+ . args ( [ "hash-object" , "-t" , "commit" , "-w" , "--stdin" ] )
222+ . current_dir ( repo_path)
223+ . stdin ( Stdio :: piped ( ) )
224+ . stdout ( Stdio :: piped ( ) )
225+ . stderr ( Stdio :: piped ( ) )
226+ . spawn ( )
227+ . expect ( "failed to run git hash-object" ) ;
228+ child
229+ . stdin
230+ . as_mut ( )
231+ . expect ( "hash-object stdin" )
232+ . write_all ( commit_content. as_bytes ( ) )
233+ . expect ( "failed to write commit content" ) ;
234+ let output = child. wait_with_output ( ) . expect ( "failed to hash commit" ) ;
235+ assert ! (
236+ output. status. success( ) ,
237+ "git hash-object failed: stdout={} stderr={}" ,
238+ String :: from_utf8_lossy( & output. stdout) ,
239+ String :: from_utf8_lossy( & output. stderr)
240+ ) ;
241+ String :: from_utf8 ( output. stdout )
242+ . expect ( "object id should be UTF-8" )
243+ . trim ( )
244+ . to_string ( )
245+ }
246+
247+ #[ test]
248+ fn walk_history_reports_missing_parent_commit ( ) {
249+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
250+ gix:: init ( temp_dir. path ( ) ) . unwrap ( ) ;
251+ run_git ( temp_dir. path ( ) , & [ "config" , "user.name" , "Test User" ] ) ;
252+ run_git (
253+ temp_dir. path ( ) ,
254+ & [ "config" , "user.email" , "test@example.com" ] ,
255+ ) ;
256+
257+ std:: fs:: write ( temp_dir. path ( ) . join ( "data.txt" ) , "base\n " ) . unwrap ( ) ;
258+ run_git ( temp_dir. path ( ) , & [ "add" , "data.txt" ] ) ;
259+ run_git ( temp_dir. path ( ) , & [ "commit" , "-m" , "base" ] ) ;
260+
261+ let tree_id = run_git ( temp_dir. path ( ) , & [ "rev-parse" , "HEAD^{tree}" ] )
262+ . trim ( )
263+ . to_string ( ) ;
264+ let missing_parent = "1111111111111111111111111111111111111111" ;
265+ let bad_commit = write_commit_object (
266+ temp_dir. path ( ) ,
267+ & format ! (
268+ "tree {tree_id}\n parent {missing_parent}\n author Test User <test@example.com> 0 +0000\n committer Test User <test@example.com> 0 +0000\n \n bad parent\n "
269+ ) ,
270+ ) ;
271+ run_git ( temp_dir. path ( ) , & [ "update-ref" , "HEAD" , & bad_commit] ) ;
272+
273+ let repo = gix:: open ( temp_dir. path ( ) ) . unwrap ( ) ;
274+ let backend = GitMetadataBackend :: new ( repo) ;
275+ let err = match backend. walk_history ( 100 ) {
276+ Ok ( history) => panic ! ( "history with a missing parent must fail, got {history:?}" ) ,
277+ Err ( err) => err,
278+ } ;
279+ assert ! (
280+ err. to_string( ) . contains( "parent" ) || err. to_string( ) . contains( "commit" ) ,
281+ "unexpected error: {err}"
282+ ) ;
283+ }
284+ }
285+
196286impl MetadataBackend for GitMetadataBackend {
197287 // ── Path access ──────────────────────────────────────────────────
198288
@@ -417,48 +507,42 @@ impl MetadataBackend for GitMetadataBackend {
417507
418508 let rev_walk = self . repo . rev_walk ( [ head_commit. id ( ) ] ) ;
419509
420- match rev_walk. all ( ) {
421- Ok ( walk) => {
422- for info in walk. take ( limit) . flatten ( ) {
423- if let Ok ( commit_obj) = info. object ( ) {
424- if let Ok ( commit_ref) = commit_obj. decode ( ) {
425- let author = match commit_ref. author ( ) {
426- Ok ( sig) => sig,
427- Err ( _) => continue ,
428- } ;
429- let committer = match commit_ref. committer ( ) {
430- Ok ( sig) => sig,
431- Err ( _) => continue ,
432- } ;
433- let timestamp = author. time ( ) . map ( |t| t. seconds ) . unwrap_or ( 0 ) ;
434- history. push ( CommitInfo {
435- id : commit_obj. id ( ) . into ( ) ,
436- author : format ! (
437- "{} <{}>" ,
438- String :: from_utf8_lossy( author. name) ,
439- String :: from_utf8_lossy( author. email)
440- ) ,
441- committer : format ! (
442- "{} <{}>" ,
443- String :: from_utf8_lossy( committer. name) ,
444- String :: from_utf8_lossy( committer. email)
445- ) ,
446- message : String :: from_utf8_lossy ( commit_ref. message ) . to_string ( ) ,
447- timestamp,
448- } ) ;
449- }
450- }
451- }
452- }
453- Err ( _) => {
454- history. push ( CommitInfo {
455- id : head_commit. id ( ) . into ( ) ,
456- author : "Unknown" . to_string ( ) ,
457- committer : "Unknown" . to_string ( ) ,
458- message : "Commit" . to_string ( ) ,
459- timestamp : 0 ,
460- } ) ;
461- }
510+ let walk = rev_walk
511+ . all ( )
512+ . map_err ( |e| GitKvError :: GitObjectError ( format ! ( "Failed to walk history: {e}" ) ) ) ?;
513+
514+ for info in walk. take ( limit) {
515+ let info = info. map_err ( |e| {
516+ GitKvError :: GitObjectError ( format ! ( "Failed to read commit history: {e}" ) )
517+ } ) ?;
518+ let commit_obj = info. object ( ) . map_err ( |e| {
519+ GitKvError :: GitObjectError ( format ! ( "Failed to load history commit: {e}" ) )
520+ } ) ?;
521+ let commit_ref = commit_obj. decode ( ) . map_err ( |e| {
522+ GitKvError :: GitObjectError ( format ! ( "Failed to decode history commit: {e}" ) )
523+ } ) ?;
524+ let author = commit_ref. author ( ) . map_err ( |e| {
525+ GitKvError :: GitObjectError ( format ! ( "Failed to decode history author: {e}" ) )
526+ } ) ?;
527+ let committer = commit_ref. committer ( ) . map_err ( |e| {
528+ GitKvError :: GitObjectError ( format ! ( "Failed to decode history committer: {e}" ) )
529+ } ) ?;
530+ let timestamp = author. time ( ) . map ( |t| t. seconds ) . unwrap_or ( 0 ) ;
531+ history. push ( CommitInfo {
532+ id : commit_obj. id ( ) . into ( ) ,
533+ author : format ! (
534+ "{} <{}>" ,
535+ String :: from_utf8_lossy( author. name) ,
536+ String :: from_utf8_lossy( author. email)
537+ ) ,
538+ committer : format ! (
539+ "{} <{}>" ,
540+ String :: from_utf8_lossy( committer. name) ,
541+ String :: from_utf8_lossy( committer. email)
542+ ) ,
543+ message : String :: from_utf8_lossy ( commit_ref. message ) . to_string ( ) ,
544+ timestamp,
545+ } ) ;
462546 }
463547
464548 Ok ( history)
0 commit comments