@@ -168,13 +168,34 @@ fn classify_message(message: &str) -> (GitErrorCategory, f32) {
168168 "could not find remote ref" ,
169169 "upstream is gone" ,
170170 "remote ref does not exist" ,
171+ "no such ref" ,
172+ "no such remote ref" ,
171173 ] ,
172174 ) || ( lower. contains ( "remote branch" )
173175 && contains_any ( & lower, & [ "not found" , "does not exist" , "missing" ] ) )
174176 {
175177 return ( GitErrorCategory :: UpstreamMissing , 0.9 ) ;
176178 }
177179
180+ if contains_any (
181+ & lower,
182+ & [
183+ "could not resolve host" ,
184+ "failed to connect" ,
185+ "connection timed out" ,
186+ "network is unreachable" ,
187+ "connection reset" ,
188+ "operation timed out" ,
189+ "temporary failure in name resolution" ,
190+ "name or service not known" ,
191+ "could not resolve hostname" ,
192+ "unable to access" ,
193+ "couldn't connect to server" ,
194+ ] ,
195+ ) {
196+ return ( GitErrorCategory :: Network , 0.85 ) ;
197+ }
198+
178199 if contains_any (
179200 & lower,
180201 & [
@@ -192,22 +213,6 @@ fn classify_message(message: &str) -> (GitErrorCategory, f32) {
192213 return ( GitErrorCategory :: Auth , 0.9 ) ;
193214 }
194215
195- if contains_any (
196- & lower,
197- & [
198- "could not resolve host" ,
199- "failed to connect" ,
200- "connection timed out" ,
201- "network is unreachable" ,
202- "connection reset" ,
203- "operation timed out" ,
204- "unable to access" ,
205- "couldn't connect to server" ,
206- ] ,
207- ) {
208- return ( GitErrorCategory :: Network , 0.85 ) ;
209- }
210-
211216 if contains_any (
212217 & lower,
213218 & [
@@ -330,6 +335,21 @@ mod tests {
330335 #[ derive( Debug ) ]
331336 struct FakeTransportError ;
332337
338+ #[ derive( Debug ) ]
339+ struct FakeAuthError ;
340+
341+ #[ derive( Debug ) ]
342+ struct FakeLockTransactionError ;
343+
344+ #[ derive( Debug ) ]
345+ struct FakeConfigError ;
346+
347+ #[ derive( Debug ) ]
348+ struct FakeReferenceRevisionError ;
349+
350+ #[ derive( Debug ) ]
351+ struct FakeFallbackError ( & ' static str ) ;
352+
333353 impl std:: fmt:: Display for FakeTransportError {
334354 fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
335355 write ! ( f, "handshake failed" )
@@ -338,6 +358,46 @@ mod tests {
338358
339359 impl Error for FakeTransportError { }
340360
361+ impl std:: fmt:: Display for FakeAuthError {
362+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
363+ write ! ( f, "credential lookup failed" )
364+ }
365+ }
366+
367+ impl Error for FakeAuthError { }
368+
369+ impl std:: fmt:: Display for FakeLockTransactionError {
370+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
371+ write ! ( f, "transaction could not write lock" )
372+ }
373+ }
374+
375+ impl Error for FakeLockTransactionError { }
376+
377+ impl std:: fmt:: Display for FakeConfigError {
378+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
379+ write ! ( f, "config value is invalid" )
380+ }
381+ }
382+
383+ impl Error for FakeConfigError { }
384+
385+ impl std:: fmt:: Display for FakeReferenceRevisionError {
386+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
387+ write ! ( f, "revision could not be resolved" )
388+ }
389+ }
390+
391+ impl Error for FakeReferenceRevisionError { }
392+
393+ impl std:: fmt:: Display for FakeFallbackError {
394+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
395+ write ! ( f, "{}" , self . 0 )
396+ }
397+ }
398+
399+ impl Error for FakeFallbackError { }
400+
341401 #[ test]
342402 fn cli_non_fast_forward_wins_before_broader_rejected_text ( ) {
343403 let interpreted = interpret_cli_error (
@@ -383,6 +443,34 @@ mod tests {
383443 assert_eq ! ( network. category, GitErrorCategory :: Network ) ;
384444 }
385445
446+ #[ test]
447+ fn cli_upstream_missing_matches_common_remote_ref_messages ( ) {
448+ let interpreted = interpret_cli_error (
449+ Some ( "push" ) ,
450+ "fatal: couldn't find remote ref refs/heads/main\n error: no such ref was fetched" ,
451+ Some ( 128 ) ,
452+ ) ;
453+
454+ assert_eq ! ( interpreted. category, GitErrorCategory :: UpstreamMissing ) ;
455+ assert ! ( interpreted. confidence >= 0.85 ) ;
456+ }
457+
458+ #[ test]
459+ fn cli_dns_style_output_maps_to_network ( ) {
460+ let interpreted = interpret_cli_error (
461+ Some ( "fetch" ) ,
462+ "ssh: Could not resolve hostname example.invalid: Name or service not known\n fatal: Could not read from remote repository." ,
463+ Some ( 128 ) ,
464+ ) ;
465+
466+ assert_eq ! ( interpreted. category, GitErrorCategory :: Network ) ;
467+ assert ! (
468+ interpreted
469+ . suggested_actions
470+ . contains( & "check-network" . to_string( ) )
471+ ) ;
472+ }
473+
386474 #[ test]
387475 fn cli_conflict_state_maps_to_sequencer_actions ( ) {
388476 let interpreted = interpret_cli_error (
@@ -433,6 +521,48 @@ mod tests {
433521 assert_eq ! ( interpreted. backend, GitBackendSource :: Gix ) ;
434522 }
435523
524+ #[ test]
525+ fn gix_typed_auth_family_maps_to_auth ( ) {
526+ let err = FakeAuthError ;
527+ let interpreted = interpret_gix_error ( Some ( "fetch" ) , & err) ;
528+
529+ assert_eq ! ( interpreted. category, GitErrorCategory :: Auth ) ;
530+ assert_eq ! ( interpreted. backend, GitBackendSource :: Gix ) ;
531+ }
532+
533+ #[ test]
534+ fn gix_typed_lock_transaction_family_maps_to_index_lock ( ) {
535+ let err = FakeLockTransactionError ;
536+ let interpreted = interpret_gix_error ( Some ( "commit" ) , & err) ;
537+
538+ assert_eq ! ( interpreted. category, GitErrorCategory :: IndexLock ) ;
539+ }
540+
541+ #[ test]
542+ fn gix_typed_config_family_maps_to_repo_state ( ) {
543+ let err = FakeConfigError ;
544+ let interpreted = interpret_gix_error ( Some ( "status" ) , & err) ;
545+
546+ assert_eq ! ( interpreted. category, GitErrorCategory :: RepoState ) ;
547+ }
548+
549+ #[ test]
550+ fn gix_typed_reference_revision_family_maps_to_repo_state ( ) {
551+ let err = FakeReferenceRevisionError ;
552+ let interpreted = interpret_gix_error ( Some ( "log" ) , & err) ;
553+
554+ assert_eq ! ( interpreted. category, GitErrorCategory :: RepoState ) ;
555+ }
556+
557+ #[ test]
558+ fn gix_fallback_message_uses_other_for_unknowns ( ) {
559+ let err = FakeFallbackError ( "unexpected internal state" ) ;
560+ let interpreted = interpret_gix_error ( Some ( "status" ) , & err) ;
561+
562+ assert_eq ! ( interpreted. category, GitErrorCategory :: Other ) ;
563+ assert ! ( interpreted. confidence < 0.5 ) ;
564+ }
565+
436566 #[ test]
437567 fn gix_opaque_message_uses_lower_confidence_heuristics ( ) {
438568 let err = std:: io:: Error :: other ( "could not lock config file" ) ;
0 commit comments