@@ -440,20 +440,7 @@ impl<'a> GitCheckout<'a> {
440440 return Ok ( ( ) ) ;
441441 }
442442
443- // Git only assumes a URL is a relative path if it starts with `./` or `../`.
444- // See [`git submodule add`] documentation.
445- //
446- // [`git submodule add`]: https://git-scm.com/docs/git-submodule
447- let child_remote_url = if [ "./" , "../" ] . iter ( ) . any ( |p| child_url_str. starts_with ( p) ) {
448- let mut new_remote_url = parent_remote_url. to_string ( ) ;
449- if !new_remote_url. ends_with ( '/' ) {
450- new_remote_url. push ( '/' ) ;
451- }
452- new_remote_url. push_str ( child_url_str) ;
453- Cow :: from ( new_remote_url)
454- } else {
455- Cow :: from ( child_url_str)
456- } ;
443+ let child_remote_url = absolute_submodule_url ( parent_remote_url, child_url_str) ?;
457444
458445 // A submodule which is listed in .gitmodules but not actually
459446 // checked out will not have a head id, so we should ignore it.
@@ -507,6 +494,58 @@ impl<'a> GitCheckout<'a> {
507494 }
508495}
509496
497+ /// Constructs an absolute URL for a child submodule URL with its parent base URL.
498+ ///
499+ /// Git only assumes a submodule URL is a relative path if it starts with `./`
500+ /// or `../` [^1]. To fetch the correct repo, we need to construct an absolute
501+ /// submodule URL.
502+ ///
503+ /// At this moment it comes with some limitations:
504+ ///
505+ /// * GitHub doesn't accept non-normalized URLs with relative paths.
506+ /// (`ssh://[email protected] /rust-lang/cargo.git/relative/..` is invalid) 507+ /// * `url` crate cannot parse SCP-like URLs.
508+ /// (`[email protected] :rust-lang/cargo.git` is not a valid WHATWG URL) 509+ ///
510+ /// To overcome these, this patch always tries [`Url::parse`] first to normalize
511+ /// the path. If it couldn't, append the relative path as the last resort and
512+ /// pray the remote git service supports non-normalized URLs.
513+ ///
514+ /// See also rust-lang/cargo#12404 and rust-lang/cargo#12295.
515+ ///
516+ /// [^1]: <https://git-scm.com/docs/git-submodule>
517+ fn absolute_submodule_url < ' s > ( base_url : & str , submodule_url : & ' s str ) -> CargoResult < Cow < ' s , str > > {
518+ let absolute_url = if [ "./" , "../" ] . iter ( ) . any ( |p| submodule_url. starts_with ( p) ) {
519+ match Url :: parse ( base_url) {
520+ Ok ( mut base_url) => {
521+ let path = base_url. path ( ) ;
522+ if !path. ends_with ( '/' ) {
523+ base_url. set_path ( & format ! ( "{path}/" ) ) ;
524+ }
525+ let absolute_url = base_url. join ( submodule_url) . with_context ( || {
526+ format ! (
527+ "failed to parse relative child submodule url `{submodule_url}` \
528+ using parent base url `{base_url}`"
529+ )
530+ } ) ?;
531+ Cow :: from ( absolute_url. to_string ( ) )
532+ }
533+ Err ( _) => {
534+ let mut absolute_url = base_url. to_string ( ) ;
535+ if !absolute_url. ends_with ( '/' ) {
536+ absolute_url. push ( '/' ) ;
537+ }
538+ absolute_url. push_str ( submodule_url) ;
539+ Cow :: from ( absolute_url)
540+ }
541+ }
542+ } else {
543+ Cow :: from ( submodule_url)
544+ } ;
545+
546+ Ok ( absolute_url)
547+ }
548+
510549/// Prepare the authentication callbacks for cloning a git repository.
511550///
512551/// The main purpose of this function is to construct the "authentication
@@ -1486,3 +1525,102 @@ fn is_short_hash_of(rev: &str, oid: Oid) -> bool {
14861525 None => false ,
14871526 }
14881527}
1528+
1529+ #[ cfg( test) ]
1530+ mod tests {
1531+ use super :: absolute_submodule_url;
1532+
1533+ #[ test]
1534+ fn test_absolute_submodule_url ( ) {
1535+ let cases = [
1536+ (
1537+ "ssh://[email protected] /rust-lang/cargo" , 1538+ "[email protected] :rust-lang/cargo.git" , 1539+ "[email protected] :rust-lang/cargo.git" , 1540+ ) ,
1541+ (
1542+ "ssh://[email protected] /rust-lang/cargo" , 1543+ "./" ,
1544+ "ssh://[email protected] /rust-lang/cargo/" , 1545+ ) ,
1546+ (
1547+ "ssh://[email protected] /rust-lang/cargo" , 1548+ "../" ,
1549+ "ssh://[email protected] /rust-lang/" , 1550+ ) ,
1551+ (
1552+ "ssh://[email protected] /rust-lang/cargo" , 1553+ "./foo" ,
1554+ "ssh://[email protected] /rust-lang/cargo/foo" , 1555+ ) ,
1556+ (
1557+ "ssh://[email protected] /rust-lang/cargo/" , 1558+ "./foo" ,
1559+ "ssh://[email protected] /rust-lang/cargo/foo" , 1560+ ) ,
1561+ (
1562+ "ssh://[email protected] /rust-lang/cargo/" , 1563+ "../foo" ,
1564+ "ssh://[email protected] /rust-lang/foo" , 1565+ ) ,
1566+ (
1567+ "ssh://[email protected] /rust-lang/cargo" , 1568+ "../foo" ,
1569+ "ssh://[email protected] /rust-lang/foo" , 1570+ ) ,
1571+ (
1572+ "ssh://[email protected] /rust-lang/cargo" , 1573+ "../foo/bar/../baz" ,
1574+ "ssh://[email protected] /rust-lang/foo/baz" , 1575+ ) ,
1576+ (
1577+ "[email protected] :rust-lang/cargo.git" , 1578+ "ssh://[email protected] /rust-lang/cargo" , 1579+ "ssh://[email protected] /rust-lang/cargo" , 1580+ ) ,
1581+ (
1582+ "[email protected] :rust-lang/cargo.git" , 1583+ "./" ,
1584+ "[email protected] :rust-lang/cargo.git/./" , 1585+ ) ,
1586+ (
1587+ "[email protected] :rust-lang/cargo.git" , 1588+ "../" ,
1589+ "[email protected] :rust-lang/cargo.git/../" , 1590+ ) ,
1591+ (
1592+ "[email protected] :rust-lang/cargo.git" , 1593+ "./foo" ,
1594+ "[email protected] :rust-lang/cargo.git/./foo" , 1595+ ) ,
1596+ (
1597+ "[email protected] :rust-lang/cargo.git/" , 1598+ "./foo" ,
1599+ "[email protected] :rust-lang/cargo.git/./foo" , 1600+ ) ,
1601+ (
1602+ "[email protected] :rust-lang/cargo.git" , 1603+ "../foo" ,
1604+ "[email protected] :rust-lang/cargo.git/../foo" , 1605+ ) ,
1606+ (
1607+ "[email protected] :rust-lang/cargo.git/" , 1608+ "../foo" ,
1609+ "[email protected] :rust-lang/cargo.git/../foo" , 1610+ ) ,
1611+ (
1612+ "[email protected] :rust-lang/cargo.git" , 1613+ "../foo/bar/../baz" ,
1614+ "[email protected] :rust-lang/cargo.git/../foo/bar/../baz" , 1615+ ) ,
1616+ ] ;
1617+
1618+ for ( base_url, submodule_url, expected) in cases {
1619+ let url = absolute_submodule_url ( base_url, submodule_url) . unwrap ( ) ;
1620+ assert_eq ! (
1621+ expected, url,
1622+ "base `{base_url}`; submodule `{submodule_url}`"
1623+ ) ;
1624+ }
1625+ }
1626+ }
0 commit comments