From 12458f67fefd55a5f1bcf2a274cf7b0e272fa8bd Mon Sep 17 00:00:00 2001 From: Seb M'Caw Date: Thu, 24 Oct 2024 15:01:02 +0100 Subject: [PATCH] feat: improve URI recognition (#1736) * Improve origin URI recognition - Consolidate a number of separate ad hoc tests for recognising URIs into Alire.URI.URI_Kind - Add support for origin URLs with scheme "ssh://" - Add bitbucket.org to the list of hosts recognised as git only - Treat ".git/" suffix the same as ".git" - Change publish command to raise errors on URLs with "file:" scheme (sidestepping a bug where the whole URL was treated as a relative path) - Change various error messages - Change handling of some obscure edge cases * Fix source archives hosted on GitHub or similar * Fix test when ssh command is unavailable * Workaround for GNAT 10 bug * Further improve URI recognition * Fix existing test * Add test for local indexes * Add test for 'clean --cache' command * Fix on Windows * Fix on MacOS * Clean up local-index-not-found test * Consolidate 'Host' implementations * Fix Fragment for local URLs * Fix test * Improve publishing test * Fix documentation * Remove redundant test * Fix tests * Fix typos * Switch to US English * Use crate_dirname * Use less fragile test for git failure * Add clarifying comments * Treat http with non-empty userinfo as private * Fix typo --- doc/catalog-format-spec.md | 9 +- src/alire/alire-index_on_disk-directory.adb | 3 +- src/alire/alire-index_on_disk-directory.ads | 11 +- src/alire/alire-index_on_disk-git.adb | 8 +- src/alire/alire-index_on_disk-git.ads | 5 +- src/alire/alire-index_on_disk.adb | 125 ++++---- src/alire/alire-index_on_disk.ads | 9 +- src/alire/alire-origins-deployers-git.adb | 2 +- src/alire/alire-origins-deployers-hg.adb | 2 +- ...alire-origins-deployers-source_archive.adb | 2 +- src/alire/alire-origins-deployers-svn.adb | 2 +- src/alire/alire-origins-tweaks.adb | 8 +- src/alire/alire-origins.adb | 219 +++++++------- src/alire/alire-origins.ads | 61 ++-- src/alire/alire-publish-submit.adb | 3 +- src/alire/alire-publish.adb | 129 ++++---- src/alire/alire-publish.ads | 2 +- src/alire/alire-roots-editable.adb | 10 +- src/alire/alire-selftest.adb | 4 +- src/alire/alire-uri.adb | 229 ++++++++++---- src/alire/alire-uri.ads | 284 +++++++++++++++--- src/alire/alire-user_pins.adb | 30 +- src/alire/alire-vcss-git.adb | 53 ++-- src/alire/alire-vcss-git.ads | 28 +- src/alire/alire-vcss-hg.adb | 9 +- src/alire/alire-vcss-hg.ads | 12 +- src/alire/alire-vcss-svn.adb | 9 +- src/alire/alire-vcss-svn.ads | 3 +- src/alire/alire-vcss.adb | 61 +--- src/alire/alire-vcss.ads | 42 +-- src/alr/alr-commands-index.adb | 10 +- src/alr/alr-commands-pin.adb | 35 ++- src/alr/alr-commands-publish.adb | 4 +- src/alr/alr-commands-withing.adb | 28 +- .../ambiguous_ssh_origin_index/index.toml | 1 + .../li/libfoo/libfoo-1.0.0.toml | 9 + .../fixtures/ssh_origin_index/index.toml | 1 + .../li/libbar/libbar-1.0.0.toml | 9 + .../li/libbaz/libbaz-1.0.0.toml | 9 + .../li/libfoo/libfoo-1.0.0.toml | 9 + testsuite/tests/clean/cache-files/test.py | 39 +++ testsuite/tests/clean/cache-files/test.yaml | 4 + .../index/git-local/my_crates/hello/hello.gpr | 10 + .../git-local/my_crates/hello/src/hello.adb | 7 + .../git-local/my_crates/libhello/libhello.gpr | 10 + .../my_crates/libhello/src/libhello.adb | 15 + .../my_crates/libhello/src/libhello.ads | 5 + .../my_index/index/he/hello/hello-1.0.1.toml | 11 + .../index/git-local/my_index/index/index.toml | 1 + .../index/li/libhello/libhello-1.0.0.toml | 8 + testsuite/tests/index/git-local/test.py | 218 ++++++++++++++ testsuite/tests/index/git-local/test.yaml | 3 + testsuite/tests/index/git-ssh-remote/test.py | 2 +- .../tests/index/local-index-not-found/test.py | 61 ++-- .../index/local-index-not-found/test.yaml | 4 - testsuite/tests/pin/bad-path/test.py | 50 ++- .../tests/pin/branch-remote-protocols/test.py | 16 +- testsuite/tests/pin/equivalent/test.py | 18 +- testsuite/tests/pin/remote/test.py | 15 +- testsuite/tests/publish/bad-arguments/test.py | 31 +- testsuite/tests/publish/check-trusted/test.py | 8 +- testsuite/tests/publish/file-scheme/test.py | 27 ++ testsuite/tests/publish/file-scheme/test.yaml | 1 + .../tests/publish/private-indexes/test.py | 127 ++++---- .../tests/publish/ssh-remote-origin/test.py | 2 + .../tests/with/ambiguous_ssh_origin/test.py | 23 ++ .../tests/with/ambiguous_ssh_origin/test.yaml | 3 + testsuite/tests/with/equivalent/test.py | 19 +- testsuite/tests/with/pin-bad-path/test.py | 50 +++ testsuite/tests/with/pin-bad-path/test.yaml | 1 + testsuite/tests/with/ssh-origins/test.py | 25 ++ testsuite/tests/with/ssh-origins/test.yaml | 3 + 72 files changed, 1587 insertions(+), 719 deletions(-) create mode 100644 testsuite/fixtures/ambiguous_ssh_origin_index/index.toml create mode 100644 testsuite/fixtures/ambiguous_ssh_origin_index/li/libfoo/libfoo-1.0.0.toml create mode 100644 testsuite/fixtures/ssh_origin_index/index.toml create mode 100644 testsuite/fixtures/ssh_origin_index/li/libbar/libbar-1.0.0.toml create mode 100644 testsuite/fixtures/ssh_origin_index/li/libbaz/libbaz-1.0.0.toml create mode 100644 testsuite/fixtures/ssh_origin_index/li/libfoo/libfoo-1.0.0.toml create mode 100644 testsuite/tests/clean/cache-files/test.py create mode 100644 testsuite/tests/clean/cache-files/test.yaml create mode 100644 testsuite/tests/index/git-local/my_crates/hello/hello.gpr create mode 100644 testsuite/tests/index/git-local/my_crates/hello/src/hello.adb create mode 100644 testsuite/tests/index/git-local/my_crates/libhello/libhello.gpr create mode 100644 testsuite/tests/index/git-local/my_crates/libhello/src/libhello.adb create mode 100644 testsuite/tests/index/git-local/my_crates/libhello/src/libhello.ads create mode 100644 testsuite/tests/index/git-local/my_index/index/he/hello/hello-1.0.1.toml create mode 100644 testsuite/tests/index/git-local/my_index/index/index.toml create mode 100644 testsuite/tests/index/git-local/my_index/index/li/libhello/libhello-1.0.0.toml create mode 100644 testsuite/tests/index/git-local/test.py create mode 100644 testsuite/tests/index/git-local/test.yaml create mode 100644 testsuite/tests/publish/file-scheme/test.py create mode 100644 testsuite/tests/publish/file-scheme/test.yaml create mode 100644 testsuite/tests/with/ambiguous_ssh_origin/test.py create mode 100644 testsuite/tests/with/ambiguous_ssh_origin/test.yaml create mode 100644 testsuite/tests/with/pin-bad-path/test.py create mode 100644 testsuite/tests/with/pin-bad-path/test.yaml create mode 100644 testsuite/tests/with/ssh-origins/test.py create mode 100644 testsuite/tests/with/ssh-origins/test.yaml diff --git a/doc/catalog-format-spec.md b/doc/catalog-format-spec.md index 2df346ffa..421cbf81b 100644 --- a/doc/catalog-format-spec.md +++ b/doc/catalog-format-spec.md @@ -447,6 +447,13 @@ static, i.e. they cannot depend on the context. the following fields: - `url`: mandatory string which points to a source file or repository. + If it points to a repository, this should be apparent from the URL; + the prefixes `git+`, `hg+` or `svn+` can be prepended to the scheme + (e.g. `git+https://`) to make this explicit, though a `.git` suffix or + the hosts `github.com`, `gitlab.com` or `bitbucket.org` will also be + recognized. For crates submitted to the community index, origins should + be publicly accessible (i.e. should not require private ssh keys or + other authentication). - `hashes`: mandatory string array for source archives. An array of "kind:digest" fields that specify a hash kind and its value. Kinds @@ -466,7 +473,7 @@ static, i.e. they cannot depend on the context. several crates from the same repository (sometimes referred to as a *monorepo*). - - `binary`: optional (defauts to false) boolean used to design the origin + - `binary`: optional (defaults to false) boolean used to design the origin as binary. Binary origins are not compiled and can optionally use dynamic expressions to narrow down the platform to which they apply. An origin using a dynamic expression must be tagged as binary; see the diff --git a/src/alire/alire-index_on_disk-directory.adb b/src/alire/alire-index_on_disk-directory.adb index d8d3f9de7..3c1d26e81 100644 --- a/src/alire/alire-index_on_disk-directory.adb +++ b/src/alire/alire-index_on_disk-directory.adb @@ -6,8 +6,7 @@ package body Alire.Index_On_Disk.Directory is overriding function Index_Directory (This : Index) return String - is (This.Origin - (This.Origin'First + File_Prefix'Length .. This.Origin'Last)); + is (URI.Local_Path (This.Origin)); ----------------- -- New_Handler -- diff --git a/src/alire/alire-index_on_disk-directory.ads b/src/alire/alire-index_on_disk-directory.ads index 661f3eedb..129c4727c 100644 --- a/src/alire/alire-index_on_disk-directory.ads +++ b/src/alire/alire-index_on_disk-directory.ads @@ -1,3 +1,5 @@ +with Alire.URI; + package Alire.Index_On_Disk.Directory is -- A local index that is taken from a local filesystem path @@ -8,10 +10,9 @@ package Alire.Index_On_Disk.Directory is function New_Handler (From : URL; Name : Restricted_Name; Parent : Any_Path) return Index with - Pre => AAA.Strings.Has_Prefix (From, "file://") - and then - Check_Absolute_Path (From (From'First + 7 .. From'Last)); - -- file:// + absolute path + Pre => Alire.URI.URI_Kind (From) in Alire.URI.File + and then Check_Absolute_Path (Alire.URI.Local_Path (From)); + -- From must be a "file:" URL with an absolute path overriding function Add (This : Index) return Outcome is (Outcome_Success); @@ -19,7 +20,7 @@ package Alire.Index_On_Disk.Directory is overriding function Index_Directory (This : Index) return String; - -- A file:// index is already on disk, so we reuse its path + -- A "file:" index is already on disk, so we reuse its path overriding function Update (This : Index) return Outcome is (Outcome_Success); diff --git a/src/alire/alire-index_on_disk-git.adb b/src/alire/alire-index_on_disk-git.adb index 1c1a6e78d..43a8fd542 100644 --- a/src/alire/alire-index_on_disk-git.adb +++ b/src/alire/alire-index_on_disk-git.adb @@ -7,9 +7,11 @@ package body Alire.Index_On_Disk.Git is --------- overriding - function Add (This : Index) return Outcome is - (VCSs.Git.Handler.Clone - (VCSs.Repo_And_Commit (This.Origin), This.Index_Directory)); + function Add (This : Index) return Outcome + is (VCSs.Git.Handler.Clone + (VCSs.Repo_URL (This.Origin), + This.Index_Directory, + VCSs.Commit (This.Origin))); ----------------- -- New_Handler -- diff --git a/src/alire/alire-index_on_disk-git.ads b/src/alire/alire-index_on_disk-git.ads index 3e1bff4c4..d996567d3 100644 --- a/src/alire/alire-index_on_disk-git.ads +++ b/src/alire/alire-index_on_disk-git.ads @@ -1,3 +1,5 @@ +with Alire.URI; + package Alire.Index_On_Disk.Git is -- Local management of remote indexes stored in git repositories @@ -8,8 +10,7 @@ package Alire.Index_On_Disk.Git is function New_Handler (Origin : URL; Name : Restricted_Name; Parent : Any_Path) return Index with - Pre => AAA.Strings.Has_Prefix (Origin, "git+") or else - AAA.Strings.Has_Prefix (Origin, "git@"); + Pre => URI.URI_Kind (Origin) in URI.Git_URIs; overriding function Add (This : Index) return Outcome; diff --git a/src/alire/alire-index_on_disk.adb b/src/alire/alire-index_on_disk.adb index 2f9901d4e..f25761e1c 100644 --- a/src/alire/alire-index_on_disk.adb +++ b/src/alire/alire-index_on_disk.adb @@ -8,7 +8,7 @@ with Alire.Index_On_Disk.Git; with Alire.Index_On_Disk.Loading; with Alire.TOML_Index; with Alire.TOML_Keys; -with Alire.VCSs; +with Alire.URI; with GNAT.OS_Lib; @@ -158,23 +158,36 @@ package body Alire.Index_On_Disk is Priority : Priorities := Default_Priority) return Index'Class is - function Process_Local_Index (Path : String) return Index'Class; - -- Check that Path designates a readable directory and create the - -- corresponding index handler for it. If something goes wrong, set - -- Result to contain the corresponding error message and return - -- New_Invalid_Index. + Origin_Kind : constant URI.URI_Kinds := URI.URI_Kind (Origin); - ------------------------- - -- Process_Local_Index -- - ------------------------- + function Abs_Path (This : String) return String; + -- Return the absolute path to which a local origin path/URL points - function Process_Local_Index (Path : String) return Index'Class is + function Is_Valid_Local_Origin (Path : String) return Boolean; + -- Return whether Path designates a readable directory suitable for an + -- index origin (i.e. not inside Alire's config path). If something goes + -- wrong, set Result to contain the corresponding error message. + + -------------- + -- Abs_Path -- + -------------- + + function Abs_Path (This : String) return String + is (Ada.Directories.Full_Name (URI.Local_Path (Origin))); + + --------------------------- + -- Is_Valid_Local_Origin -- + --------------------------- + + function Is_Valid_Local_Origin (Path : String) return Boolean is use GNATCOLL.VFS; Dir : constant Virtual_File := Create (+Path); begin + -- Ensure the path exists and is a directory + if not Dir.Is_Directory then Result := Outcome_Failure ("Not a readable directory: " & Path); - return New_Invalid_Index; + return False; end if; -- Ensure the given path is not one of our own configured indexes @@ -182,17 +195,11 @@ package body Alire.Index_On_Disk is if AAA.Strings.Has_Prefix (Path, Parent) then Result := Outcome_Failure ("Given index path is inside Alire configuration path"); - return New_Invalid_Index; + return False; end if; - -- Ensure the created Index wrapper has absolute path - - Result := Outcome_Success; - return Directory.New_Handler - (File_Prefix & Ada.Directories.Full_Name (Path), - Name, - Parent).With_Priority (Priority); - end Process_Local_Index; + return True; + end Is_Valid_Local_Origin; begin if not Is_Valid_Name (Name) then @@ -200,53 +207,51 @@ package body Alire.Index_On_Disk is return New_Invalid_Index; end if; - -- Warn about http[s]:// URLs being not supported and suggest git+http - -- instead. - - if AAA.Strings.Has_Prefix (Origin, HTTP_Prefix) then - Result := Outcome_Failure - ("HTTP/HTTPS URLs are not valid index origins. " - & "You may want git+" & Origin & " instead."); - return New_Invalid_Index; - end if; - - -- Process "file://" URLs and anything that looks like a file name as a - -- local index. - - if AAA.Strings.Has_Prefix (Origin, File_Prefix) then - return Process_Local_Index - (Origin (Origin'First + File_Prefix'Length .. Origin'Last)); - elsif Origin (Origin'First) = '/' - or else not (AAA.Strings.Contains (Origin, "@") - or else AAA.Strings.Contains (Origin, "+")) + if Origin_Kind in URI.Local_URIs + and then not Is_Valid_Local_Origin (URI.Local_Path (Origin)) then - return Process_Local_Index (Origin); - end if; - - -- Process "git+ssh://" as git over ssh and suggest for "ssh://" - - if AAA.Strings.Has_Prefix (Origin, SSH_Prefix) then - Result := Outcome_Failure - ("ssh:// URLs are not valid index origins. " - & "You may want git+" & Origin & " instead."); + -- Result is already set to Outcome_Failure by Is_Valid_Local_Origin return New_Invalid_Index; - elsif AAA.Strings.Has_Prefix (Origin, "git+" & SSH_Prefix) then - Result := Outcome_Success; - return Index_On_Disk.Git - .New_Handler (Origin, Name, Parent) - .With_Priority (Priority); end if; - -- Process other paths as VCSs - - case VCSs.Kind (Origin) is - when VCSs.VCS_Git => + case Origin_Kind is + when URI.Local_Other => + -- Process path or "file:" URL as a local index. Result := Outcome_Success; - return Index_On_Disk.Git.New_Handler (Origin, Name, Parent) - .With_Priority (Priority); - when VCSs.VCS_Unknown => + return Index_On_Disk.Directory.New_Handler + (URI.To_URL (Abs_Path (Origin)), Name, Parent) + .With_Priority (Priority); + + when URI.Git_URIs => + -- A recognized Git repo; create a clone in the config directory. + declare + -- Ensure `git+file:` origin is an absolute path + From : constant String := + (if Origin_Kind in URI.Local_Git + then URI.Make_VCS_Explicit (Abs_Path (Origin), URI.Git) + else Origin); + begin + Result := Outcome_Success; + return Index_On_Disk.Git.New_Handler (From, Name, Parent) + .With_Priority (Priority); + end; + + when URI.Hg_URIs | URI.SVN_URIs => + -- Other VCSs are not currently supported. Result := Outcome_Failure ("Unknown index kind: " & Origin); return New_Invalid_Index; + + when URI.HTTP_Other | URI.SSH_Other => + -- Warn that URL is not recognized and suggest 'git+http' or + -- 'git+ssh' instead. + Result := Outcome_Failure + ("Unrecognized index URL. Did you mean 'git+" + & Origin & "' instead?"); + return New_Invalid_Index; + + when others => + Result := Outcome_Failure ("Unrecognized index URL: " & Origin); + return New_Invalid_Index; end case; end New_Handler; diff --git a/src/alire/alire-index_on_disk.ads b/src/alire/alire-index_on_disk.ads index 061c48778..0a6046e12 100644 --- a/src/alire/alire-index_on_disk.ads +++ b/src/alire/alire-index_on_disk.ads @@ -13,17 +13,14 @@ package Alire.Index_On_Disk is -- Index metadata are stored in the /indexes//index.toml -- Actual index is stored in /indexes//repo - -- URLs given to New_Handler functions must be complete, commit optional: + -- URLs given to New_Handler functions must be complete, optionally with a + -- commit (except for 'file:' URLs, where path can contain literal '#'): -- E.g.: git+https://path/to/server/and/project[#commit] - -- E.g.: file:///path/to/local/folder + -- E.g.: file:/path/to/local/folder Checkout_Directory : constant String := "repo"; Metadata_Filename : constant String := "index.toml"; - File_Prefix : constant String := "file://"; - HTTP_Prefix : constant String := "http"; - SSH_Prefix : constant String := "ssh://"; - subtype Priorities is Integer; -- Lower is loaded before Default_Priority : constant := 1; diff --git a/src/alire/alire-origins-deployers-git.adb b/src/alire/alire-origins-deployers-git.adb index 2fc62c453..d43423dff 100644 --- a/src/alire/alire-origins-deployers-git.adb +++ b/src/alire/alire-origins-deployers-git.adb @@ -13,7 +13,7 @@ package body Alire.Origins.Deployers.Git is overriding function Deploy (This : Deployer; Folder : String) return Outcome is begin - VCSs.Git.Handler.Clone (This.Base.URL_With_Commit, Folder).Assert; + VCSs.Git.Handler.Clone (This.Base.URL, Folder, This.Base.Commit).Assert; if Settings.Builtins.Dependencies_Git_Keep_Repository.Get then diff --git a/src/alire/alire-origins-deployers-hg.adb b/src/alire/alire-origins-deployers-hg.adb index 1e80fe4dc..6d22268b0 100644 --- a/src/alire/alire-origins-deployers-hg.adb +++ b/src/alire/alire-origins-deployers-hg.adb @@ -9,7 +9,7 @@ package body Alire.Origins.Deployers.Hg is overriding function Deploy (This : Deployer; Folder : String) return Outcome is begin - return VCSs.Hg.Handler.Clone (This.Base.URL_With_Commit, Folder); + return VCSs.Hg.Handler.Clone (This.Base.URL, Folder, This.Base.Commit); end Deploy; ----------- diff --git a/src/alire/alire-origins-deployers-source_archive.adb b/src/alire/alire-origins-deployers-source_archive.adb index 9128d4b05..fa06f24fe 100644 --- a/src/alire/alire-origins-deployers-source_archive.adb +++ b/src/alire/alire-origins-deployers-source_archive.adb @@ -138,7 +138,7 @@ package body Alire.Origins.Deployers.Source_Archive is -- linux also for local files, something funny is going on Windows which -- is difficult to pinpoint. - if URI.Scheme (This.Base.Archive_URL) in URI.File_Schemes then + if URI.URI_Kind (This.Base.Archive_URL) in URI.Local_Other then if not Dirs.Exists (Folder) then Alire.Directories.Create_Tree (Folder); end if; diff --git a/src/alire/alire-origins-deployers-svn.adb b/src/alire/alire-origins-deployers-svn.adb index ac2c05c15..1af649747 100644 --- a/src/alire/alire-origins-deployers-svn.adb +++ b/src/alire/alire-origins-deployers-svn.adb @@ -9,7 +9,7 @@ package body Alire.Origins.Deployers.SVN is overriding function Deploy (This : Deployer; Folder : String) return Outcome is begin - return VCSs.SVN.Handler.Clone (This.Base.URL_With_Commit, Folder); + return VCSs.SVN.Handler.Clone (This.Base.URL, Folder, This.Base.Commit); end Deploy; ----------- diff --git a/src/alire/alire-origins-tweaks.adb b/src/alire/alire-origins-tweaks.adb index aa9fcabb3..ef57aab1c 100644 --- a/src/alire/alire-origins-tweaks.adb +++ b/src/alire/alire-origins-tweaks.adb @@ -37,10 +37,10 @@ package body Alire.Origins.Tweaks is function Fix_VCS return Origin is use Ada.Directories; - URL : constant String := This.URL; -- Doesn't include #commit + URL : constant String := This.URL; begin - -- Check for "xxx+file://" or return as-is: - if URI.Scheme (URL) not in URI.File_Schemes then + -- Return as-is unless local path: + if URI.URI_Kind (URL) not in URI.Local_URIs then return This; end if; @@ -55,7 +55,7 @@ package body Alire.Origins.Tweaks is -- Rebuild the filesystem path as absolute for the VCS in hand: Absolute.Data.Repo_URL := + -- Unbounded string - (Prefix_File & Full_Name (TOML_Path / Rel_Path)); + (Full_Name (TOML_Path / Rel_Path)); return Absolute; end; diff --git a/src/alire/alire-origins.adb b/src/alire/alire-origins.adb index 15f130356..d43b25b4b 100644 --- a/src/alire/alire-origins.adb +++ b/src/alire/alire-origins.adb @@ -137,6 +137,20 @@ package body Alire.Origins is function URL (This : Origin) return Alire.URL is (Alire.URL (+This.Data.Repo_URL)); + + ------------------ + -- Explicit_URL -- + ------------------ + + function Explicit_URL (This : Origin) return Alire.URL + is (URI.Make_VCS_Explicit + (This.URL, + (case This.Kind is + when Git => URI.Git, + when Hg => URI.Hg, + when SVN => URI.SVN, + when others => raise Program_Error with "unreachable case"))); + ------------ -- Commit -- ------------ @@ -144,13 +158,6 @@ package body Alire.Origins is function Commit (This : Origin) return String is (+This.Data.Commit); - --------------------- - -- URL_With_Commit -- - --------------------- - - function URL_With_Commit (This : Origin) return Alire.URL is - (This.URL & "#" & This.Commit); - ------------------------- -- TTY_URL_With_Commit -- ------------------------- @@ -206,7 +213,7 @@ package body Alire.Origins is is (case This.Kind is when Filesystem => This.Path, when Source_Archive => This.Archive_URL, - when VCS_Kinds => This.URL, + when VCS_Kinds => This.Explicit_URL, when others => raise Checked_Error with "Origin has no URL"); -------------- @@ -364,36 +371,20 @@ package body Alire.Origins is -- We add the "file:" to have a proper URI and simplify things for -- Windows absolute paths with drive letter. - return (Data => (Source_Archive, - Src_Archive => - (URL => - +(if URI.Scheme (URL) in URI.File_Schemes - then "file:" & Ada.Directories.Full_Name - (URI.Local_Path (URL)) - else URL), - Name => +Archive_Name, - Format => Format, - Binary => False, - Hashes => <>))); + return (Data => + (Source_Archive, + Src_Archive => + (URL => + +(if URI.URI_Kind (URL) in URI.Local_Other + then URI.To_URL + (Ada.Directories.Full_Name (URI.Local_Path (URL))) + else URL), + Name => +Archive_Name, + Format => Format, + Binary => False, + Hashes => <>))); end New_Source_Archive; - ----------------- - -- From_String -- - ----------------- - - function From_String (Image : String) return Origin is - Scheme : constant URI.Schemes := URI.Scheme (Image); - begin - case Scheme is - when URI.File_Schemes => - return New_Filesystem (URI.Local_Path (Image)); - when URI.HTTP => - return New_Source_Archive (Image); - when others => - Raise_Checked_Error ("Unsupported URL scheme: " & Image); - end case; - end From_String; - ------------- -- New_VCS -- ------------- @@ -401,50 +392,31 @@ package body Alire.Origins is function New_VCS (URL : Alire.URL; Commit : String; Subdir : Relative_Path := "") return Origin is - use AAA.Strings; - use all type URI.Schemes; - Scheme : constant URI.Schemes := URI.Scheme (URL); - VCS_URL : constant String := - (if Contains (URL, "+file://") then - Tail (URL, '+') -- strip the VCS proto only - elsif Contains (URL, "+file:") then - Tail (URL, ':') -- Remove file: w.o. // that confuses git - elsif Scheme = URI.Pure_Git then - URL - elsif Scheme in URI.VCS_Schemes then - Tail (URL, '+') -- remove prefix vcs+ - elsif Scheme in URI.HTTP then -- A plain URL... check VCS - (if Has_Suffix (To_Lower_Case (URL), ".git") - then URL - elsif VCSs.Git.Known_Transformable_Hosts.Contains - (URI.Authority (URL)) - then URL & ".git" - else raise Checked_Error with - "ambiguous VCS URL: " & URL) - else - raise Checked_Error with "unknown VCS URL: " & URL); + URL_Kind : constant URI.URI_Kinds := URI.URI_Kind (URL); + VCS_URL : constant String := VCSs.Repo_URL (URL); begin - case Scheme is - when Pure_Git | Git | HTTP => - if not Is_Valid_Commit (Commit) then + case URL_Kind is + when URI.Git_URIs => + if not VCSs.Git.Is_Valid_Commit (Commit) then Raise_Checked_Error ("invalid git commit id, " & "40 digits hexadecimal expected"); end if; return New_Git (VCS_URL, Commit, Subdir); - when Hg => - if not Is_Valid_Mercurial_Commit (Commit) then + when URI.Hg_URIs => + if not VCSs.Hg.Is_Valid_Commit (Commit) then Raise_Checked_Error ("invalid mercurial commit id, " & "40 digits hexadecimal expected"); end if; return New_Hg (VCS_URL, Commit, Subdir); - when SVN => + when URI.SVN_URIs => return New_SVN (VCS_URL, Commit, Subdir); + when URI.HTTP_Other | URI.SSH_Other => + Raise_Checked_Error ("ambiguous VCS URL: " & URL); when others => - Raise_Checked_Error ("Expected a VCS origin but got scheme: " - & Scheme'Image); + Raise_Checked_Error ("unknown VCS URL: " & URL); end case; end New_VCS; @@ -555,7 +527,7 @@ package body Alire.Origins is is use TOML; - use all type URI.Schemes; + use all type URI.URI_Kinds; Table : constant TOML_Adapters.Key_Queue := From.Descend (From.Checked_Pop (Keys.Origin, TOML_Table), Context => Keys.Origin); @@ -569,6 +541,27 @@ package body Alire.Origins is Data.Binary := True; end Mark_Binary; + ------------------------- + -- Load_Source_Archive -- + ------------------------- + + procedure Load_Source_Archive (This : in out Origin; + Table : TOML_Adapters.Key_Queue; + URL : String) is + begin + -- Reinsert the URL so we can reuse the dynamic archive loader: + Table.Unwrap.Set (Keys.URL, Create_String (URL)); + + -- And load + This := (Data => (Kind => Source_Archive, + Src_Archive => From_TOML + (Table.Descend + (Keys.Origin, + Table.Unwrap, + Context => "source archive")), + Hashes => <>)); + end Load_Source_Archive; + begin -- Check if we are seeing a conditional binary origin, or a regular -- static one. If the former, divert to the dynamic loader; else @@ -603,53 +596,56 @@ package body Alire.Origins is -- Regular static loading of other origin kinds declare - URL : constant String := + URL : constant String := Table.Checked_Pop (Keys.URL, TOML_String).As_String; - Scheme : constant URI.Schemes := URI.Scheme (URL); - Hashed : constant Boolean := Table.Unwrap.Has (Keys.Hashes); + URL_Kind : constant URI.URI_Kinds := URI.URI_Kind (URL); + Hashed : constant Boolean := Table.Unwrap.Has (Keys.Hashes); begin - case Scheme is - when External => + case URL_Kind is + when External => This := New_External (URI.Path (URL)); - when URI.File_Schemes => + when URI.Local_Other => if URI.Local_Path (URL) = "" then From.Checked_Error ("empty path given in local origin"); end if; This := New_Filesystem (URI.Local_Path (URL)); - when URI.VCS_Schemes => - declare - Commit : constant String := Table.Checked_Pop - (Keys.Commit, TOML_String).As_String; - Subdir : constant String := - (if Table.Contains (Keys.Subdir) - then Table.Checked_Pop - (Keys.Subdir, TOML_String).As_String - else ""); - begin - This := New_VCS - (URL, - Commit => Commit, - Subdir => VFS.To_Native (Portable_Path (Subdir))); - end; + when URI.VCS_URIs => + if URL_Kind in URI.Probably_Git and then Hashed then + -- To resolve the ambiguity of Probably_Git, assume a source + -- archive if the "hashes" field is present. + Load_Source_Archive (This, Table, URL); + else + -- In all other cases, treat this as a git repo. + declare + Commit : constant String := Table.Checked_Pop + (Keys.Commit, TOML_String).As_String; + Subdir : constant String := + (if Table.Contains (Keys.Subdir) + then Table.Checked_Pop + (Keys.Subdir, TOML_String).As_String + else ""); + begin + This := New_VCS + (URL, + Commit => Commit, + Subdir => VFS.To_Native (Portable_Path (Subdir))); + end; + end if; - when HTTP => - -- Reinsert the URL so we can reuse the dynamic archive loader: - Table.Unwrap.Set (Keys.URL, Create_String (URL)); - - -- And load - This := (Data => (Kind => Source_Archive, - Src_Archive => From_TOML - (Table.Descend - (Keys.Origin, - Table.Unwrap, - Context => "source archive")), - Hashes => <>)); - when System => + when URI.HTTP_Other => + Load_Source_Archive (This, Table, URL); + + when SSH_Other => + From.Checked_Error ("Pure 'ssh://' URLs are not valid crate " + & "origins. You may want git+" & URL + & " instead."); + + when System => This := New_System (URI.Path (URL)); - when Unknown => + when Unknown => From.Checked_Error ("unsupported scheme in URL: " & URL); end case; @@ -765,20 +761,10 @@ package body Alire.Origins is begin case This.Kind is when Filesystem => - Table.Set (Keys.URL, +("file:" & This.Path)); + Table.Set (Keys.URL, +(URI.To_URL (This.Path))); when VCS_Kinds => - Table.Set (Keys.URL, - +((if This.Kind in Git - and then AAA.Strings.Has_Prefix (This.URL, "git@") - then "" - else Prefixes (This.Kind).all) - & (if URI.Scheme (This.URL) in URI.None - -- not needed for remote repos, but for testing - -- ones used locally: - then "file:" - else "") - & This.URL)); + Table.Set (Keys.URL, +This.Explicit_URL); Table.Set (Keys.Commit, +This.Commit); if This.Subdir /= "" then Table.Set (Keys.Subdir, @@ -787,8 +773,7 @@ package body Alire.Origins is when External => Table.Set (Keys.URL, - +(Prefixes (This.Kind).all & - (+This.Data.Description))); + +(Prefix_External & (+This.Data.Description))); when Binary_Archive => Table := TOML_Adapters.Merge_Tables @@ -802,7 +787,7 @@ package body Alire.Origins is when System => Table.Set (Keys.URL, - +(Prefixes (This.Kind).all & This.Package_Name)); + +(Prefix_System & This.Package_Name)); end case; if not This.Get_Hashes.Is_Empty then diff --git a/src/alire/alire-origins.ads b/src/alire/alire-origins.ads index cf67cd64d..9d986a920 100644 --- a/src/alire/alire-origins.ads +++ b/src/alire/alire-origins.ads @@ -12,7 +12,6 @@ private with Ada.Containers.Indefinite_Vectors; private with Ada.Strings.Unbounded; with TOML; use all type TOML.Any_Value_Kind; -with Alire.Utils; package Alire.Origins is @@ -30,10 +29,6 @@ package Alire.Origins is type Kinds_Set is array (Kinds) of Boolean with Default_Component_Value => False; - type String_Access is access constant String; - type Prefix_Array is array (Kinds) of String_Access; - Prefixes : constant Prefix_Array; - subtype Archive_Kinds is Kinds with Static_Predicate => Archive_Kinds in Binary_Archive | Source_Archive; @@ -75,17 +70,31 @@ package Alire.Origins is function Commit (This : Origin) return String with Pre => This.Kind in VCS_Kinds; + function Subdir (This : Origin) return Relative_Path with Pre => This.Kind in VCS_Kinds; -- Returns "" or a path in which the crate is located within the deployed -- origin. + function URL (This : Origin) return Alire.URL with Pre => This.Kind in VCS_Kinds; - function URL_With_Commit (This : Origin) return Alire.URL + -- The URL which is passed to the VCS. + -- + -- Has no "vcs+" prefix to disambiguate the type of VCS, and a local URL is + -- a path (i.e. "/some/path", not "file:/some/path"). + + function Explicit_URL (This : Origin) return Alire.URL with Pre => This.Kind in VCS_Kinds; - -- Append commit as '#commit' + -- A version of the URL which makes the appropriate VCS recognizable. + -- + -- For example, if This.URL is "https://host/path", and This.Kind is Git, + -- then This.Explicit_URL will be "git+https://host/path". Likewise, if + -- This.URL is "/some/path", then This.Explicit_URL will be + -- "git+file:/some/path". + function TTY_URL_With_Commit (This : Origin) return String with Pre => This.Kind in VCS_Kinds; + function Is_Monorepo (This : Origin) return Boolean; function Path (This : Origin) return String @@ -93,10 +102,13 @@ package Alire.Origins is function Archive_URL (This : Origin) return Alire.URL with Pre => This.Kind in Archive_Kinds; + function Archive_Name (This : Origin) return String with Pre => This.Kind in Archive_Kinds; + function Archive_Format (This : Origin) return Known_Source_Archive_Format with Pre => This.Kind in Archive_Kinds; + function Archive_Format (Name : String) return Source_Archive_Format; -- Guess the format of a source archive from its file name. @@ -104,6 +116,7 @@ package Alire.Origins is Pre => This.Kind in Filesystem | Source_Archive | VCS_Kinds; function Is_System (This : Origin) return Boolean is (This.Kind = System); + function Package_Name (This : Origin) return String with Pre => This.Kind = System; @@ -126,14 +139,6 @@ package Alire.Origins is subtype Git_Commit is VCSs.Git.Git_Commit; subtype Hg_Commit is VCSs.Hg.Hg_Commit; - function Is_Valid_Commit (S : String) return Boolean - is (S'Length = Git_Commit'Length and then - (for all Char of S => Char in Alire.Utils.Hexadecimal_Character)); - - function Is_Valid_Mercurial_Commit (S : String) return Boolean - is (S'Length = Hg_Commit'Length and then - (for all Char of S => Char in Alire.Utils.Hexadecimal_Character)); - function Short_Commit (Commit : String) return String; -- First characters in the commit @@ -163,8 +168,8 @@ package Alire.Origins is function New_VCS (URL : Alire.URL; Commit : String; Subdir : Relative_Path := "") return Origin; - -- Attempt to identify an origin kind from the transport (git+https). If no - -- VCS specified, look for ".git" extension. + -- Determine whether URL looks like git, Hg or SVN, and construct an origin + -- accordingly. Raises Checked_Error if not recognized as any VCS. Unknown_Source_Archive_Name_Error : exception; @@ -191,12 +196,6 @@ package Alire.Origins is procedure Add_Hash (This : in out Origin; Hash : Hashes.Any_Hash); - function From_String (Image : String) return Origin with - Post => From_String'Result.Kind in Filesystem | Source_Archive; - -- Parse a string and dispatch to the appropriate constructor. This - -- function can be used to retrieve unhashed origins too (precisely - -- for hashing). - overriding function From_TOML (This : in out Origin; From : TOML_Adapters.Key_Queue) @@ -300,6 +299,9 @@ private when VCS_Kinds => Repo_URL : Unbounded_String; + -- The URL which is passed to the VCS. Any "vcs+" prefixes are + -- removed, and 'file:/some/path' URLs are converted into + -- "/some/path" local paths. Commit : Unbounded_String; Subdir : Unbounded_Relative_Path; @@ -322,21 +324,8 @@ private function Image_Of_Hashes (This : Origin) return String; Prefix_External : aliased constant String := "external:"; - Prefix_Git : aliased constant String := "git+"; - Prefix_Hg : aliased constant String := "hg+"; - Prefix_SVN : aliased constant String := "svn+"; - Prefix_File : aliased constant String := "file://"; Prefix_System : aliased constant String := "system:"; - Prefixes : constant Prefix_Array := - (Git => Prefix_Git'Access, - Hg => Prefix_Hg'Access, - SVN => Prefix_SVN'Access, - External => Prefix_External'Access, - Filesystem => Prefix_File'Access, - System => Prefix_System'Access, - Archive_Kinds => null); - function Is_Monorepo (This : Origin) return Boolean is (This.Kind in VCS_Kinds and then This.Subdir /= ""); diff --git a/src/alire/alire-publish-submit.adb b/src/alire/alire-publish-submit.adb index dcfc96885..9ca0cd1ee 100644 --- a/src/alire/alire-publish-submit.adb +++ b/src/alire/alire-publish-submit.adb @@ -261,12 +261,13 @@ package body Alire.Publish.Submit is Directories.Force_Delete (Local_Repo_Path); -- Delete a possibly outdated repo - VCSs.Git.Handler.Clone + VCSs.Git.Handler.Clone_Branch (From => Index.Community_Host / User_Info.User_GitHub_Login / Index.Community_Repo_Name, Into => Local_Repo_Path, Branch => "", + Commit => "", Depth => 1).Assert; -- We can reuse the pull logic now to set up the local branch diff --git a/src/alire/alire-publish.adb b/src/alire/alire-publish.adb index b62352360..109f8160b 100644 --- a/src/alire/alire-publish.adb +++ b/src/alire/alire-publish.adb @@ -807,7 +807,7 @@ package body Alire.Publish is function Get_Default (Remote_URL : String) return Answer_Kind - is (if Force or else URI.Scheme (Remote_URL) in URI.HTTP + is (if Force or else URI.URI_Kind (Remote_URL) in URI.HTTP_Other then Yes else No); @@ -920,22 +920,20 @@ package body Alire.Publish is -- Ensure the origin is remote - if URI.Scheme (URL) not in URI.HTTP then - -- A git@ URL is private to the user and should not be used for - -- packaging via the the community index - if AAA.Strings.Has_Prefix (URL, "git@") then - if not Context.Options.For_Private_Index then - Raise_Checked_Error - ("The origin cannot use a private remote: " & URL); - end if; - - -- Otherwise we assume this is a local path - else - Recoverable_User_Error - ("The origin must be a definitive remote location, but is " & URL); - -- For testing we may want to allow local URLs, or may be for - -- internal use with network drives? So allow forcing it. + if URI.URI_Kind (URL) in URI.Unknown then + Raise_Checked_Error ("Unsupported scheme: " & URL); + elsif URI.URI_Kind (URL) in URI.Private_URIs then + -- A private URL should not be used for packaging via the community + -- index + if not Context.Options.For_Private_Index then + Raise_Checked_Error + ("The origin cannot use a private remote: " & URL); end if; + elsif URI.URI_Kind (URL) not in URI.Public_URIs then + Recoverable_User_Error + ("The origin must be a definitive remote location, but is " & URL); + -- For testing we may want to allow local URLs, or may be for + -- internal use with network drives? So allow forcing it. end if; Put_Success ("Origin is of supported kind: " & Context.Origin.Kind'Img); @@ -946,10 +944,7 @@ package body Alire.Publish is -- a local repository. if (Force and then - URI.Scheme (URL) in URI.File_Schemes | URI.Unknown) - -- We are forcing, so we accept an unknown scheme (this happens - -- for local file on Windows, where drive letters are interpreted - -- as the scheme). + URI.URI_Kind (URL) in URI.Local_URIs) or else Is_Trusted (URL) then @@ -1208,49 +1203,15 @@ package body Alire.Publish is (Root_Path, Origin => Git.Remote (Root_Path)); -- The one reported by the repo, in its public form - - Fetch_URL : constant String := - -- With an added ".git", if it hadn't one. Not usable in local - -- filesystem. - Raw_URL - & (if Has_Suffix (To_Lower_Case (Raw_URL), ".git") - then "" - else ".git"); begin - -- To allow this call to succeed with local tests, we check - -- here. For a regular repository we will already have an HTTP - -- transport. A GIT transport is not wanted, because that one - -- requires the owner keys. - case URI.Scheme (Fetch_URL) is - when URI.VCS_Schemes => - if Options.For_Private_Index then - Publish.Remote_Origin (URL => Raw_URL, - Commit => Commit, - Subdir => +Subdir, - Options => Options); - else - Raise_Checked_Error - ("The remote URL seems to require repository " - & "ownership: " & Fetch_URL); - end if; - when URI.None | URI.Unknown => - Publish.Remote_Origin (URL => "git+file:" & Raw_URL, - Commit => Commit, - Subdir => +Subdir, - Options => Options); - when URI.File => - Publish.Remote_Origin (URL => Raw_URL, - Commit => Commit, - Subdir => +Subdir, - Options => Options); - when URI.HTTP => - Publish.Remote_Origin (URL => Fetch_URL, - Commit => Commit, - Subdir => +Subdir, - Options => Options); - when others => - Raise_Checked_Error ("Unsupported scheme: " & Fetch_URL); - end case; + -- Complete the process with Remote_Origin, adding a "git+" + -- prefix if necessary to ensure that the URL will be + -- recognized as a git remote + Publish.Remote_Origin (URL => URI.Make_VCS_Explicit + (Raw_URL, URI.Git), + Commit => Commit, + Subdir => +Subdir, + Options => Options); end; end; end; @@ -1265,14 +1226,25 @@ package body Alire.Publish is Subdir : Relative_Path := ""; Options : All_Options := New_Options) is + Kind : constant URI.URI_Kinds := URI.URI_Kind (URL); begin -- Preliminary argument checks - if Has_Suffix (AAA.Strings.To_Lower_Case (URL), ".git") and then - Commit = "" - then - Raise_Checked_Error - ("URL seems to point to a repository, but no commit was provided."); + -- Check that a commit is provided if the URL is definitely a VCS repo + -- + -- If the URL is only probably a repository, we raise a warning but + -- otherwise assume this is meant to be a source archive (since + -- Origins.New_Source_Archive will raise an error if this turns out not + -- to be the case). + + if Commit = "" and then Kind in URI.VCS_URIs then + if Kind in URI.Probably_Git then + Put_Warning ("Assuming origin is a source archive " + & "because no commit was provided."); + else + Raise_Checked_Error ("URL seems to point to a repository, " + & "but no commit was provided."); + end if; end if; -- Verify that subdir is not used with an archive (it is only for VCSs) @@ -1281,6 +1253,17 @@ package body Alire.Publish is Raise_Checked_Error ("Cannot publish a nested crate from an archive"); end if; + -- Check for obviously invalid url + + if Kind in URI.SSH_Other then + Raise_Checked_Error ("'ssh://' URLs are not valid crate origins. You " + & "may want git+" & URL & " instead."); + end if; + if Kind in URI.External | URI.System | URI.Unknown + then + Raise_Checked_Error ("Unsupported scheme: " & URL); + end if; + -- Create origin, which will do more checks, and proceed declare @@ -1292,15 +1275,11 @@ package body Alire.Publish is (if Commit /= "" then Origins.New_VCS (URL, Commit, Subdir) - -- without commit - elsif URI.Scheme (URL) in URI.VCS_Schemes or else - VCSs.Git.Known_Transformable_Hosts.Contains - (URI.Authority_Without_Credentials (URL)) - then - raise Checked_Error with - "A commit id is mandatory for a VCS origin" - -- plain archive + -- + -- From the preliminary argument checks above, the + -- absence of a commit implies URL doesn't look like + -- a VCS. else Origins.New_Source_Archive (URL)), diff --git a/src/alire/alire-publish.ads b/src/alire/alire-publish.ads index 9c892a4c9..a634b9253 100644 --- a/src/alire/alire-publish.ads +++ b/src/alire/alire-publish.ads @@ -25,7 +25,7 @@ package Alire.Publish is procedure Local_Repository (Path : Any_Path := "."; Revision : String := "HEAD"; Options : All_Options := New_Options) with - Pre => URI.Scheme (Path) in URI.File_Schemes; + Pre => URI.URI_Kind (Path) in URI.Bare_Path; -- Check that given Path is an up-to-date git repo. If so, proceed with -- remote repo verification. If no revision given use the HEAD commit, -- otherwise use the revision (tag, branch, commit) commit. diff --git a/src/alire/alire-roots-editable.adb b/src/alire/alire-roots-editable.adb index a54d4f020..ab8817eab 100644 --- a/src/alire/alire-roots-editable.adb +++ b/src/alire/alire-roots-editable.adb @@ -5,7 +5,6 @@ with Alire.Dependencies.Diffs; with Alire.Directories; with Alire.Index; with Alire.Manifest; -with Alire.Origins; with Alire.Roots.Optional; with Alire.User_Pins; with Alire.Utils.User_Input; @@ -405,7 +404,7 @@ package body Alire.Roots.Editable is -- We accept any reference that can be converted to a commit, as commit. -- This is a bit of a misnomer really in the command-line interface. - if Ref /= "" and then not Origins.Is_Valid_Commit (Ref) then + if Ref /= "" and then not VCSs.Git.Is_Valid_Commit (Ref) then Convert_Ref_To_Commit; return; end if; @@ -413,12 +412,11 @@ package body Alire.Roots.Editable is -- Clone the remote so we can identify the crate and perform other -- validity checks. - if not VCSs.Git.Handler.Clone - (From => Origin & (if Ref /= "" - then "#" & Ref - else ""), + if not VCSs.Git.Handler.Clone_Branch + (From => Origin, Into => Temp_Pin.Filename, Branch => Branch, -- May be empty for default branch + Commit => Ref, -- May be empty for most recent commit Depth => 1).Success then Raise_Checked_Error diff --git a/src/alire/alire-selftest.adb b/src/alire/alire-selftest.adb index a6ae87802..a6f8c69bd 100644 --- a/src/alire/alire-selftest.adb +++ b/src/alire/alire-selftest.adb @@ -136,7 +136,7 @@ package body Alire.Selftest is begin -- Proper transform starting without .git pragma Assert (Transform_To_Public ("git@github.com:user/project") = - "https://github.com/user/project.git"); + "git+https://github.com/user/project"); -- Proper transform starting with .git pragma Assert (Transform_To_Public ("git@github.com:user/project.git") = @@ -144,7 +144,7 @@ package body Alire.Selftest is -- GitLab pragma Assert (Transform_To_Public ("git@gitlab.com:user/project") = - "https://gitlab.com/user/project.git"); + "git+https://gitlab.com/user/project"); -- Unknown site, not transformed pragma Assert (Transform_To_Public ("git@ggithub.com:user/project") = diff --git a/src/alire/alire-uri.adb b/src/alire/alire-uri.adb index afd90830f..6129773ff 100644 --- a/src/alire/alire-uri.adb +++ b/src/alire/alire-uri.adb @@ -7,78 +7,75 @@ package body Alire.URI is function Authority_Without_Credentials (This : URL) return String is Auth : constant String := Authority (This); begin - if (for some Char of Auth => Char = '@') then - return AAA.Strings.Tail (Auth, '@'); + if Contains (Auth, "@") then + return Tail (Auth, '@'); else return Auth; end if; end Authority_Without_Credentials; - ------------ - -- Scheme -- - ------------ + -------------- + -- Userinfo -- + -------------- - function Scheme (This : URL) return Schemes - is - Img : constant String := L (U.Scheme (This)); + function Userinfo (This : URL) return String is + Auth : constant String := Authority (This); begin - return - (if Img = "" then - None - elsif Img = "external" then - External - elsif Img = "file" then - File - elsif AAA.Strings.Has_Prefix (Img, "git+") then - Git - elsif AAA.Strings.Has_Prefix (Img, "git@") then - Pure_Git - elsif AAA.Strings.Has_Prefix (Img, "hg+") then - Hg - elsif AAA.Strings.Has_Prefix (Img, "svn+") then - SVN - elsif Img = "http" then - HTTP - elsif Img = "https" then - HTTP - elsif Img = "system" then - System - elsif Img'Length = 1 and then Img (Img'First) in 'a' .. 'z' then - None -- A Windows drive letter, so a path without scheme + if Contains (Auth, "@") then + return Head (Auth, '@'); + else + return ""; + end if; + end Userinfo; + + ------------------- + -- Host_From_URL -- + ------------------- + + -- Return the host part of a URL. + -- + -- Doesn't check whether This is actually a URL. + function Host_From_URL (This : URL) return String is + Auth : constant String := Authority_Without_Credentials (This); + begin + -- Return Auth with any trailing port removed (note that the host may be + -- an IPv6 address in square brackets) + if Has_Prefix (Auth, "[") then + if Contains (Auth, "]:") then + return Head (Auth, "]:") & "]"; else - Unknown); - end Scheme; + return Auth; + end if; + else + return Head (Auth, ":"); + end if; + end Host_From_URL; ---------- -- Host -- ---------- function Host (This : URL) return String is - use AAA.Strings; - Auth : constant String := Authority_Without_Credentials (This); begin - if Scheme (This) in File_Schemes then - return ""; - elsif Has_Prefix (This, "git@") - and then not Contains (Head (This, ":"), "/") - then + if URI_Kind (This) in SCP_Style_Git then -- This has the form git@X:Y, so return X return Head (Tail (This, "@"), ":"); + elsif URI_Kind (This) in Non_URLs | Local_URIs then + -- Return empty string for other non-URLs or local URLs + return ""; else - -- This is a normal URI, so return with any trailing port removed - -- (note that the host may be an IPv6 address in square brackets) - if Has_Prefix (Auth, "[") then - if Contains (Auth, "]:") then - return Head (Auth, "]:") & "]"; - else - return Auth; - end if; - else - return Head (Auth, ":"); - end if; + return Host_From_URL (This); end if; end Host; + -------------- + -- Fragment -- + -------------- + + function Fragment (This : URL) return String + is (if URI_Kind (This) in Non_URLs | Local_URIs then "" + else U.Extract (This, U.Fragment)); + package body Operators is --------- @@ -90,4 +87,134 @@ package body Alire.URI is end Operators; + -------------- + -- URI_Kind -- + -------------- + + function URI_Kind (This : String) return URI_Kinds is + Scheme : constant String := L (U.Scheme (This)); + Has_Userinfo : constant Boolean := (Userinfo (This) /= ""); + begin + if Scheme = "external" then + return External; + elsif Scheme = "system" then + return System; + elsif Scheme = "" then + return Bare_Path; + elsif Scheme = "file" then + return File; + elsif Has_Prefix (Scheme, "git@") then + -- We use the fact that URI schemes must not contain a '/', so this + -- condition is only satisfied if there is no '/' before the ':', as + -- required by git (https://git-scm.com/docs/git-clone#URLS) + return SCP_Style_Git; + elsif Scheme = "git+https" or else Scheme = "git+http" then + return (if Has_Userinfo + then Private_Definitely_Git + else Public_Definitely_Git); + elsif Scheme = "git+file" then + return Local_Git; + elsif Has_Prefix (Scheme, "git+") then + return Private_Definitely_Git; + elsif Scheme = "hg+https" or else Scheme = "hg+http" then + return Public_Hg; + elsif Scheme = "hg+file" then + return Local_Hg; + elsif Has_Prefix (Scheme, "hg+") then + return Private_Hg; + elsif Scheme = "svn+https" or else Scheme = "svn+http" then + return Public_SVN; + elsif Scheme = "svn+file" then + return Local_SVN; + elsif Has_Prefix (Scheme, "svn+") then + return Private_SVN; + elsif Scheme = "http" or else Scheme = "https" then + if Has_Git_Suffix (This) then + return (if Has_Userinfo + then Private_Definitely_Git + else Public_Definitely_Git); + elsif Is_Known_Git_Host (Host_From_URL (This)) then + -- These are known git hosts, so recognize them even without a + -- ".git" suffix + return (if Has_Userinfo + then Private_Probably_Git + else Public_Probably_Git); + else + return (if Has_Userinfo + then Private_HTTP_Other + else Public_HTTP_Other); + end if; + elsif Scheme = "ssh" then + if Has_Git_Suffix (This) then + return Private_Definitely_Git; + elsif Is_Known_Git_Host (Host_From_URL (This)) then + -- These are known git hosts (over SSH, so This can't be a raw + -- file), so recognize them even without a ".git" suffix + return Private_Definitely_Git; + else + return SSH_Other; + end if; + elsif Scheme'Length = 1 and then Scheme (Scheme'First) in 'a' .. 'z' then + -- Something starting with "C:" or similar is probably a windows path + return Bare_Path; + else + return Unknown; + end if; + end URI_Kind; + + ------------------------ + -- Strip_VCS_Prefixes -- + ------------------------ + + function Strip_VCS_Prefixes (This : String) return String is + Scheme : constant String := L (U.Scheme (This)); + begin + if (for some P of VCS_Prefixes => Has_Prefix (Scheme, P.all)) then + return Tail (This, '+'); + else + return This; + end if; + end Strip_VCS_Prefixes; + + ----------------------- + -- Make_VCS_Explicit -- + ----------------------- + + function Make_VCS_Explicit (This : String; Kind : VCS_Kinds) return String + is + VCS_Prefix : constant String := VCS_Prefixes (Kind).all; + Current_Kind : constant URI_Kinds := URI_Kind (This); + begin + case Current_Kind is + when Bare_Path => + -- Convert "/some/path" to "vcs+file:/some/path" + return VCS_Prefix & To_URL (This); + when HTTP_Other | SSH_Other | File => + -- Not recognizable, so prepend prefix + return VCS_Prefix & This; + when VCS_URIs => + if VCS_Kind (This) /= Kind then + raise Program_Error + with "URL already looks like a different VCS"; + elsif Current_Kind = SCP_Style_Git then + -- git@host:/path is already explicit + return This; + elsif Current_Kind in Probably_Git then + -- Prepend prefix to make it *_Definitely_Git + return VCS_Prefix & This; + else + -- This is already recognized as the correct VCS, so do nothing + return This; + end if; + when others => + raise Program_Error with "Inappropriate VCS URL"; + end case; + end Make_VCS_Explicit; + + ------------------- + -- In_Local_URIs -- + ------------------- + + function In_Local_URIs (K : URI_Kinds) return Boolean + is (K in Local_URIs); end Alire.URI; diff --git a/src/alire/alire-uri.ads b/src/alire/alire-uri.ads index 4265896d7..4d0375b11 100644 --- a/src/alire/alire-uri.ads +++ b/src/alire/alire-uri.ads @@ -31,45 +31,147 @@ package Alire.URI with Preelaborate is end Operators; - type Schemes is - (None, - -- For URLs without scheme (to be interpreted as local paths) + type URI_Kinds is + (External, + -- Scheme is "external:" + -- Denotes a crate detected by some external definition - External, - -- external: denotes a crate detected by some external definition - - File, - -- A file: URI + System, + -- Scheme is "system:" + -- The form "system:package" is used to denote a native package from + -- the platform + + Local_Git, + -- Scheme is "git+file:" + + Public_Probably_Git, + -- Scheme is "http:"/"https:", userinfo component is empty or absent and + -- host is known to serve git repos + -- + -- Such URLs are occasionally raw file downloads, not repositories, but + -- will never be a non-git VCS. + + Public_Definitely_Git, + -- Scheme is "git+http"/"git+https", or scheme is "http:"/"https:" with + -- ".git" or ".git/" suffix + + SCP_Style_Git, + -- An SCP-like git remote, with the form git@host:path + + Private_Probably_Git, + -- Scheme is "http:"/"https:", userinfo component is non-empty and host + -- is known to serve git repos + -- + -- Such URLs are occasionally raw file downloads, not repositories, but + -- will never be a non-git VCS. + + Private_Definitely_Git, + -- Recognizably git and scheme is "ssh:", or scheme + -- is "git+:" (where proto is not "http"/"https" or "file") + + Local_Hg, + Public_Hg, + Private_Hg, + Local_SVN, + Public_SVN, + Private_SVN, + -- Same considerations as for Git - Git, - -- Anything understood by git, expressed as git+, e.g.: - -- git+http[s], git+file + Bare_Path, + -- A local path, not a URI (note that this is often still a git repo, it + -- just isn't apparent from the string alone) - Pure_Git, - -- An actual git@host:path URI + File, + -- Scheme is "file:" - Hg, - SVN, - -- Same considerations as for Git + Public_HTTP_Other, + -- http/https, but not a recognized VCS - HTTP, - -- Either http or https, since we don't differentiate treatment + Private_HTTP_Other, + -- Scheme is http/https, userinfo is non-empty and not a recognized VCS - System, - -- system:package is used to denote a native package from the platform + SSH_Other, + -- ssh, but not a recognized VCS Unknown -- Anything else ); - -- Protocols recognized by Alire - subtype VCS_Schemes is Schemes range Git .. SVN; + subtype Local_Other is URI_Kinds range Bare_Path .. File; + + subtype HTTP_Other is URI_Kinds + range Public_HTTP_Other .. Private_HTTP_Other; + + subtype Public_Git is URI_Kinds + range Public_Probably_Git .. Public_Definitely_Git; + -- Note that this includes the ambiguous case Public_Probably_Git + + subtype Private_Git is URI_Kinds + range SCP_Style_Git .. Private_Definitely_Git; + -- Note that this includes the ambiguous case Public_Probably_Git + + subtype Probably_Git is URI_Kinds + with Static_Predicate => + Probably_Git in Private_Probably_Git | Public_Probably_Git; + + subtype Private_Other is URI_Kinds range Private_HTTP_Other .. SSH_Other; + + subtype Non_URLs is URI_Kinds + with Static_Predicate => + Non_URLs in External | System | SCP_Style_Git | Bare_Path | Unknown; + + subtype VCS_URIs is URI_Kinds range Local_Git .. Private_SVN; + -- Note that this includes the ambiguous cases Public_Probably_Git and + -- Private_Probably_Git + + subtype Local_VCS_URIs is URI_Kinds + with Static_Predicate => + Local_VCS_URIs in Local_Git | Local_Hg | Local_SVN; + + subtype Local_URIs is URI_Kinds + with Static_Predicate => + Local_URIs in Local_VCS_URIs | Local_Other; + + subtype Public_VCS_URIs is URI_Kinds + with Static_Predicate => + Public_VCS_URIs in Public_Git | Public_Hg | Public_SVN; + -- Note that this includes the ambiguous case Public_Probably_Git + + subtype Public_URIs is URI_Kinds + with Static_Predicate => + Public_URIs in Public_VCS_URIs | Public_HTTP_Other; + + subtype Private_VCS_URIs is URI_Kinds + with Static_Predicate => + Private_VCS_URIs in Private_Git | Private_Hg | Private_SVN; + -- Note that this includes the ambiguous case Private_Probably_Git + + subtype Private_URIs is URI_Kinds + with Static_Predicate => + Private_URIs in Private_VCS_URIs | Private_Other; + + subtype Git_URIs is VCS_URIs range Local_Git .. Private_Definitely_Git; + -- Note that this includes the ambiguous cases Public_Probably_Git and + -- Private_Probably_Git + + subtype Hg_URIs is VCS_URIs range Local_Hg .. Private_Hg; + + subtype SVN_URIs is VCS_URIs range Local_SVN .. Private_SVN; + + function URI_Kind (This : String) return URI_Kinds; + -- Attempt to identify the nature of a resource from its URI. + -- + -- Formats currently not recognized include: + -- user@host:/path/to/repo.git [returns Unknown] + -- host.name:/path/to/repo.git [returns Unknown] + -- git://host/path/to/repo.git [returns Unknown] + -- ftp://host/path/to/repo.git [returns Unknown] + -- svn://host/path/to/repo [returns Unknown] - subtype File_Schemes is Schemes with - Static_Predicate => File_Schemes in None | File; + type VCS_Kinds is (Git, Hg, SVN); - function Scheme (This : URL) return Schemes; - -- Extract the Scheme part of a URL + function VCS_Kind (This : String) return VCS_Kinds + with Pre => URI_Kind (This) in VCS_URIs; function Authority (This : URL) return String; -- The authority includes credentials : user:pass@websi.te @@ -77,44 +179,106 @@ package Alire.URI with Preelaborate is function Authority_Without_Credentials (This : URL) return String; -- Only the part after @ in an authority + function Userinfo (This : URL) return String; + -- Only the part before @ in an authority + -- + -- Returns an empty string if there is no @. + function Host (This : URL) return String; - -- The host part of a remote URI + -- The host part of a remote URL -- -- Remotes of the form 'git@host.name:/some/path' (which are not valid -- URIs) return the 'host.name' part. -- - -- Returns an empty string for local URIs. + -- Returns an empty string for other non-URLs or local filesystem URLs. + + function Fragment (This : URL) return String; + -- The fragment part of a URL + -- + -- Returns empty string for non-URLs or local filesystem URLs, or if there + -- is no fragment. function Local_Path (This : URL) return String - with Pre => Scheme (This) in None | File + with Pre => In_Local_URIs (URI_Kind (This)) or else raise Checked_Error with Errors.Set ("Given URL does not seem to denote a path: " & This); - -- Extract complete path from a URL intended for a local path: According to - -- the URIs RFC, we (I) are using improperly the file: scheme. An absolute - -- path should be file:/path/to/file, while a relative one should be - -- file:rel/ati/ve. By using things like file://../path/to, ".." becomes - -- the authority and "/path/to" the absolute path. This function, for use - -- with Alire, returns the authority+path as the whole path, so there's no - -- possible misinterpretation and any file:[/[/[/]]] combination should be - -- properly interpreted. TL;DR: this should work without further concerns. + -- Extract complete path from a "file:" URL; return a path unchanged. -- - -- TODO: fix incorrectly emitted file:// paths in Origins so at least we - -- are not generating improper URIs. + -- We are using the "file:" scheme improperly: according to RFC 8089 + -- (https://tools.ietf.org/html/rfc8089), an absolute path should be either + -- "file:/path/to/file" or "file:///path/to/file", a relative path is not + -- permitted, and any "?" or "#" character terminates the path (defining a + -- query or fragment respectively). This function, for use with Alire, + -- accepts either "file:rel/ati/ve" or "file://rel/ati/ve" as relative + -- paths, in addition to the aforementioned absolute path formats, and + -- treats "?" and "#" characters as literal components of the path. + -- + -- Percent-encoding is not decoded. function Path (This : URL) return String; -- The path as properly defined (without the authority, if any) - function Is_HTTP_Or_Git (This : URL) return Boolean - is (Scheme (This) in Git | Pure_Git | HTTP - or else AAA.Strings.Has_Suffix (This, ".git")); - -- Heuristic to detect a possible git remote. Implementation public so - -- there is no doubt to what it does. + function To_URL (Path : Any_Path) return URL + with Pre => URI_Kind (Path) = Bare_Path; + -- The 'file:' URL equivalent of a local path + -- + -- Like Local_Path, this does not strictly adhere to RFC 8089. + -- Specifically, percent-encoding is not performed, so paths containing + -- '?' and '#' may cause issues. + + function Strip_VCS_Prefixes (This : String) return String; + -- Return the URL without any "git+" prefix or similar. + + function Make_VCS_Explicit (This : String; Kind : VCS_Kinds) return String + with Post => URI_Kind (Make_VCS_Explicit'Result) in VCS_URIs; + -- Return the URL minimally modified to make the VCS recognizable. + -- + -- For example, This => "https://host/path" with Kind => Git returns + -- "git+https://host/path", and This => "/some/path" with Kind => Hg + -- returns "hg+file:/some/path". + -- + -- Changes are only made if necessary, so This => "https://host/path.git" + -- and Kind => Git returns the URL unmodified. + -- + -- Raises Program_Error if the URL already looks like a different VCS + -- or has a URI_Kind of External or System. + + function In_Local_URIs (K : URI_Kinds) return Boolean; + -- For use by this package only + -- + -- Equivalent to "K in Local_URIs", but used as a workaround for the fact + -- that GNAT 10 seems to fall over if a subtype with a static predicate is + -- referenced in the same file as its definition private + use AAA.Strings; + package U renames Standard.URI; - function L (Str : String) return String renames AAA.Strings.To_Lower_Case; + type String_Access is access constant String; + type Prefix_Array is array (VCS_Kinds) of String_Access; + + Prefix_Git : aliased constant String := "git+"; + Prefix_Hg : aliased constant String := "hg+"; + Prefix_SVN : aliased constant String := "svn+"; + + VCS_Prefixes : constant Prefix_Array := (Git => Prefix_Git'Access, + Hg => Prefix_Hg'Access, + SVN => Prefix_SVN'Access); + + function L (Str : String) return String renames To_Lower_Case; + + -------------- + -- VCS_Kind -- + -------------- + + function VCS_Kind (This : String) return VCS_Kinds + is (case URI_Kind (This) is + when Git_URIs => Git, + when Hg_URIs => Hg, + when SVN_URIs => SVN, + when others => raise Program_Error with "unreachable case"); --------------- -- Authority -- @@ -128,10 +292,8 @@ private ---------------- function Local_Path (This : URL) return String - is (case Scheme (This) is - when None => This, - when File => U.Permissive_Path (This), - when others => raise Program_Error with "not applicable"); + is (if URI_Kind (This) in Bare_Path then This + else U.Extract (This, U.Authority, U.Fragment)); ---------- -- Path -- @@ -140,4 +302,28 @@ private function Path (This : URL) return String is (U.Extract (This, U.Path)); + ------------ + -- To_URL -- + ------------ + + function To_URL (Path : Any_Path) return URL + is ("file:" & Path); + + -------------------- + -- Has_Git_Suffix -- + -------------------- + + function Has_Git_Suffix (URL : String) return Boolean + is (Has_Suffix (L (URL), ".git") or else Has_Suffix (L (URL), ".git/")); + + ----------------------- + -- Is_Known_Git_Host -- + ----------------------- + + function Is_Known_Git_Host (Host : String) return Boolean + is (Host = "github.com" + or else Host = "gitlab.com" + or else Host = "bitbucket.org"); + -- Return whether a string is a known host which definitely uses git + end Alire.URI; diff --git a/src/alire/alire-user_pins.adb b/src/alire/alire-user_pins.adb index cc9a4f725..8aedcc700 100644 --- a/src/alire/alire-user_pins.adb +++ b/src/alire/alire-user_pins.adb @@ -7,7 +7,6 @@ with Alire.Utils.User_Input; with Alire.Utils.TTY; with Alire.VFS; -with Ada.Strings.Unbounded; with AAA.Strings; with GNAT.OS_Lib; @@ -170,12 +169,11 @@ package body Alire.User_Pins is & "..."); if not - VCSs.Git.Handler.Clone - (From => URL (This) & (if Commit /= "" - then "#" & Commit - else ""), + VCSs.Git.Handler.Clone_Branch + (From => URL (This), Into => Temp.Filename, Branch => Branch, -- May be empty for default branch + Commit => Commit, -- May be empty for most recent commit Depth => 1).Success then Raise_Checked_Error @@ -202,10 +200,14 @@ package body Alire.User_Pins is & TTY.URL (Destination)); -- If the fetch URL has been changed, fall back to checkout + -- + -- Note that VCSs.Git.Clone converts the URL to a git-friendly form + -- with VCSs.Repo, so this is what the output of 'git config' should + -- be compared against. if VCSs.Git.Handler.Fetch_URL (Repo => Destination, - Public => False) /= This.URL + Public => False) /= VCSs.Repo_URL (URL (This)) then Put_Info ("Switching pin " & Utils.TTY.Name (Crate) & " to origin at " & TTY.URL (+This.URL)); @@ -469,7 +471,6 @@ package body Alire.User_Pins is function Load_Remote return Pin is use Ada.Strings.Unbounded; - use AAA.Strings; Result : Pin := (Kind => To_Git, URL => +This.Checked_Pop (Keys.URL, @@ -478,21 +479,6 @@ package body Alire.User_Pins is Commit => <>, Local_Path => <>); begin - -- "git+ssh://"" and "ssh+git://" are deprecated, so treat them as - -- identical to "ssh://" - if Has_Prefix (To_String (Result.URL), "git+ssh://") - or else Has_Prefix (To_String (Result.URL), "ssh+git://") - then - Replace_Slice (Result.URL, 1, 7, "ssh"); - end if; - - -- Likewise, anything of the form "xyz+https://" should be treated - -- as just "https://" - if Contains (To_String (Result.URL), "+http") then - Result.URL := To_Unbounded_String - (Tail (To_String (Result.URL), '+')); - end if; - if This.Contains (Keys.Branch) and then This.Contains (Keys.Commit) then diff --git a/src/alire/alire-vcss-git.adb b/src/alire/alire-vcss-git.adb index f819ac365..d90cd8dfa 100644 --- a/src/alire/alire-vcss-git.adb +++ b/src/alire/alire-vcss-git.adb @@ -3,6 +3,7 @@ with Ada.Directories; with Alire.Directories; with Alire.OS_Lib.Subprocess; with Alire.Errors; +with Alire.URI; with Alire.Utils.Tools; with Alire.Utils.User_Input.Query_Config; with Alire.VFS; @@ -127,21 +128,23 @@ package body Alire.VCSs.Git is overriding function Clone (This : VCS; From : URL; - Into : Directory_Path) - return Outcome - is (This.Clone (From, Into, Branch => "", Depth => 1)); - - ----------- - -- Clone -- - ----------- - - not overriding - function Clone (This : VCS; - From : URL; - Into : Directory_Path; - Branch : String; - Depth : Natural := 0) + Into : Directory_Path; + Commit : String := "") return Outcome + is (This.Clone_Branch + (From, Into, Branch => "", Commit => Commit, Depth => 1)); + + ------------------ + -- Clone_Branch -- + ------------------ + + function Clone_Branch (This : VCS; + From : URL; + Into : Directory_Path; + Branch : String; + Commit : String := ""; + Depth : Natural := 0) + return Outcome is pragma Unreferenced (This); Extra : constant Vector := @@ -149,7 +152,7 @@ package body Alire.VCSs.Git is then Empty_Vector & "-q" else Empty_Vector); Depth_Opts : constant Vector := - (if Depth /= 0 and then Commit (From) = "" + (if Depth /= 0 and then Commit = "" then Empty_Vector & "--depth" & Trim (Depth'Image) & "--no-single-branch" -- but all tips else Empty_Vector); @@ -161,16 +164,16 @@ package body Alire.VCSs.Git is Trace.Detail ("Checking out [git]: " & From); Run_Git (Empty_Vector & "clone" & "--recursive" & - Extra & Branch_Opts & Depth_Opts & Repo (From) & Into); + Extra & Branch_Opts & Depth_Opts & Repo_URL (From) & Into); - if Commit (From) /= "" then + if Commit /= "" then declare Guard : Directories.Guard (Directories.Enter (Into)) with Unreferenced; begin -- Checkout a specific commit. -- "-q" needed to avoid the "detached HEAD" warning from git - Run_Git (Empty_Vector & "checkout" & "-q" & Commit (From)); + Run_Git (Empty_Vector & "checkout" & "-q" & Commit); -- Update the submodules, if any Run_Git (Empty_Vector & "submodule" & "update" & @@ -182,7 +185,7 @@ package body Alire.VCSs.Git is exception when E : others => return Alire.Errors.Get (E); - end Clone; + end Clone_Branch; ------------- -- Command -- @@ -586,16 +589,14 @@ package body Alire.VCSs.Git is ------------------------- function Transform_To_Public (Remote : String) return URL is - Domain : constant String := Head (Tail (Remote, '@'), ':'); + Domain : constant String := URI.Host (Remote); begin - if Has_Prefix (Remote, "git@") and then + if URI.URI_Kind (Remote) in URI.SCP_Style_Git and then Known_Transformable_Hosts.Contains (Domain) then - return Public : constant URL := - "https://" & Domain & "/" & Tail (Remote, ':') - & (if Has_Suffix (Remote, ".git") - then "" - else ".git") + return Public : constant URL := URI.Make_VCS_Explicit + ("https://" & Domain & "/" & Tail (Remote, ':'), + URI.Git) do Trace.Warning ("Private git " & TTY.URL (Remote) & " transformed to public " & TTY.URL (Public)); diff --git a/src/alire/alire-vcss-git.ads b/src/alire/alire-vcss-git.ads index da4d5d526..2f69029d6 100644 --- a/src/alire/alire-vcss-git.ads +++ b/src/alire/alire-vcss-git.ads @@ -12,8 +12,7 @@ package Alire.VCSs.Git is (for all Char of Git_Commit => Char in Utils.Hexadecimal_Character); function Is_Valid_Commit (S : String) return Boolean - is (S'Length = Git_Commit'Length and then - (for all Char of S => Char in Utils.Hexadecimal_Character)); + is (S in Git_Commit); No_Commit : constant Git_Commit := (others => '0'); -- This is actually returned by e.g. `git worktree`, even if it could be a @@ -48,21 +47,22 @@ package Alire.VCSs.Git is overriding function Clone (This : VCS; From : URL; - Into : Directory_Path) - return Outcome; - -- Make a shallow clone of the given URL (that may include '#commit). For - -- more precise control, use the following Clone signature. - - not overriding - function Clone (This : VCS; - From : URL; - Into : Directory_Path; - Branch : String; - Depth : Natural := 0) + Into : Directory_Path; + Commit : String := "") return Outcome; + -- Make a shallow clone of the given URL, optionally specifying a commit. + -- For more precise control, use the following Clone signature. + + function Clone_Branch (This : VCS; + From : URL; + Into : Directory_Path; + Branch : String; + Commit : String := ""; + Depth : Natural := 0) + return Outcome; -- Specify a branch to check out after cloning. Branch may be "" for the -- default remote branch. For any Depth /= 0, apply --depth . A - -- commit may be specified as From#Commit_Id + -- commit may optionally be specified. type Output is new AAA.Strings.Vector with null record; diff --git a/src/alire/alire-vcss-hg.adb b/src/alire/alire-vcss-hg.adb index fca9e1e1f..ec09200cb 100644 --- a/src/alire/alire-vcss-hg.adb +++ b/src/alire/alire-vcss-hg.adb @@ -16,7 +16,8 @@ package body Alire.VCSs.Hg is overriding function Clone (This : VCS; From : URL; - Into : Directory_Path) + Into : Directory_Path; + Commit : String := "") return Outcome is pragma Unreferenced (This); @@ -27,8 +28,8 @@ package body Alire.VCSs.Hg is else "-v"); Commit_Arg : constant Vector := - (if Commit (From) /= "" - then Empty_Vector & "-u" & Commit (From) + (if Commit /= "" + then Empty_Vector & "-u" & Commit else Empty_Vector); begin @@ -45,7 +46,7 @@ package body Alire.VCSs.Hg is "-y" & Commit_Arg & Extra & - Repo (From) & + Repo_URL (From) & Into); return Outcome_Success; diff --git a/src/alire/alire-vcss-hg.ads b/src/alire/alire-vcss-hg.ads index 0f541a5d5..891fcec89 100644 --- a/src/alire/alire-vcss-hg.ads +++ b/src/alire/alire-vcss-hg.ads @@ -1,6 +1,13 @@ +with Alire.Utils; + package Alire.VCSs.Hg is - subtype Hg_Commit is String (1 .. 40); + subtype Hg_Commit is String (1 .. 40) with + Dynamic_Predicate => + (for all Char of Hg_Commit => Char in Utils.Hexadecimal_Character); + + function Is_Valid_Commit (S : String) return Boolean + is (S in Hg_Commit); type VCS (<>) is new VCSs.VCS with private; @@ -9,7 +16,8 @@ package Alire.VCSs.Hg is overriding function Clone (This : VCS; From : URL; - Into : Directory_Path) + Into : Directory_Path; + Commit : String := "") return Outcome; overriding diff --git a/src/alire/alire-vcss-svn.adb b/src/alire/alire-vcss-svn.adb index 256fc5f57..3f544b2f2 100644 --- a/src/alire/alire-vcss-svn.adb +++ b/src/alire/alire-vcss-svn.adb @@ -13,7 +13,8 @@ package body Alire.VCSs.SVN is overriding function Clone (This : VCS; From : URL; - Into : Directory_Path) + Into : Directory_Path; + Commit : String := "") return Outcome is pragma Unreferenced (This); @@ -23,8 +24,8 @@ package body Alire.VCSs.SVN is else Empty_Vector); Commit_Arg : constant Vector := - (if Commit (From) /= "" - then Empty_Vector & String'("-r" & Commit (From)) + (if Commit /= "" + then Empty_Vector & String'("-r" & Commit) else Empty_Vector); begin Trace.Detail ("Checking out [svn]: " & From); @@ -37,7 +38,7 @@ package body Alire.VCSs.SVN is Empty_Vector & "checkout" & Extra & - Repo (From) & + Repo_URL (From) & Commit_Arg & Into); return Outcome_Success; diff --git a/src/alire/alire-vcss-svn.ads b/src/alire/alire-vcss-svn.ads index f4c26a4dd..082ca7d8e 100644 --- a/src/alire/alire-vcss-svn.ads +++ b/src/alire/alire-vcss-svn.ads @@ -7,7 +7,8 @@ package Alire.VCSs.SVN is overriding function Clone (This : VCS; From : URL; - Into : Directory_Path) + Into : Directory_Path; + Commit : String := "") return Outcome; overriding diff --git a/src/alire/alire-vcss.adb b/src/alire/alire-vcss.adb index 52ba6c29b..280c8d9b2 100644 --- a/src/alire/alire-vcss.adb +++ b/src/alire/alire-vcss.adb @@ -1,60 +1,25 @@ -with Alire.VCSs.Git; +with Alire.URI; package body Alire.VCSs is use AAA.Strings; - ----------- - -- Clone -- - ----------- - - function Clone (From : URL; - Into : Directory_Path) - return Outcome - is - begin - case Kind (From) is - when VCS_Git => - return Git.Handler.Clone (Repo_And_Commit (From), Into); - when VCS_Unknown => - return Outcome_Failure ("Unknown VCS requested: " & From); - end case; - end Clone; - ------------ -- Commit -- ------------ function Commit (Origin : URL) return String is - (if Contains (Origin, "#") - then Tail (Origin, '#') - else ""); - - ---------- - -- Kind -- - ---------- - - function Kind (Origin : URL) return Kinds is - (if Has_Prefix (Origin, "git+") or else Has_Prefix (Origin, "git@") - then VCS_Git - else VCS_Unknown); - - ---------- - -- Repo -- - ---------- - - function Repo (Origin : URL) return String is - (Head (Repo_And_Commit (Origin), '#')); - - --------------------- - -- Repo_And_Commit -- - --------------------- - - function Repo_And_Commit (Origin : URL) return String - is (if Contains (Origin, "+http") or else Has_Prefix (Origin, "git+") - then Tail (Origin, '+') - elsif Has_Prefix (Origin, "file://") - then Origin (Origin'First + 7 .. Origin'Last) - else Origin); + (URI.Fragment (Origin)); + + -------------- + -- Repo_URL -- + -------------- + + function Repo_URL (Origin : URL) return String is + (if URI.URI_Kind (Origin) in URI.Non_URLs + then Origin + elsif URI.URI_Kind (Origin) in URI.Local_URIs + then URI.Local_Path (Origin) + else Head (URI.Strip_VCS_Prefixes (Origin), '#')); end Alire.VCSs; diff --git a/src/alire/alire-vcss.ads b/src/alire/alire-vcss.ads index 1197b0520..cbdd36fe1 100644 --- a/src/alire/alire-vcss.ads +++ b/src/alire/alire-vcss.ads @@ -2,16 +2,10 @@ package Alire.VCSs is type VCS is interface; - type Kinds is (VCS_Git, VCS_Unknown); - - subtype Known_Kinds is Kinds range Kinds'First .. Kinds'Pred (VCS_Unknown); - - -- URL format: - -- vcs+http[s]://path/to/repo[#commit] - - function Clone (This : VCS; - From : URL; - Into : Directory_Path) + function Clone (This : VCS; + From : URL; + Into : Directory_Path; + Commit : String := "") return Outcome is abstract; function Update (This : VCS; @@ -22,25 +16,17 @@ package Alire.VCSs is -- General utilities -- ----------------------- - function Kind (Origin : URL) return Kinds; - - function Repo (Origin : URL) return String; - -- Without kind and commit - - function Repo_And_Commit (Origin : URL) return String; - -- Without Kind and with optional Commit (separated by #) + function Repo_URL (Origin : URL) return String; + -- Return a URL suitable for passing directly to a VCS + -- + -- Strips any "vcs+" prefix, converts "file:" URLs to paths and removes any + -- fragment part (separated by #). + -- + -- Returns non-URLs unmodified. function Commit (Origin : URL) return String; - -- Empty string if no commit part (separated by #) - - ------------------------ - -- Classwide versions -- - ------------------------ - -- Those redispatch to the appropriate descendant - - function Clone (From : URL; - Into : Directory_Path) - return Outcome - with Pre => Kind (From) in Known_Kinds; + -- The fragment of the URL (separated by #) + -- + -- Returns empty string for non-URLs or if there is no fragment. end Alire.VCSs; diff --git a/src/alr/alr-commands-index.adb b/src/alr/alr-commands-index.adb index cb212a177..1908adbe4 100644 --- a/src/alr/alr-commands-index.adb +++ b/src/alr/alr-commands-index.adb @@ -209,10 +209,12 @@ package body Alr.Commands.Index is .New_Line .Append ("URL can be one of:") .Append ("- Plain absolute path: /path/to/index") - .Append ("- Explicit path: file://path/to/index") - .Append ("- git over HTTP/HTTPS: git+https://github.com/org/repo") - .Append ("- git over SSH: git+ssh://user@host.com:/path/to/repo") - .Append ("- git user over SSH: git@github.com:/org/repo") + .Append ("- Explicit path: file:/path/to/index") + .Append ("- git over HTTP/HTTPS: " + & "git+https://github.com/org/repo[#commit]") + .Append ("- git over SSH: " + & "git+ssh://[user@]host.com/path/to/repo[#commit]") + .Append ("- git user over SSH: git@github.com:/org/repo[#commit]") ); --------------------- diff --git a/src/alr/alr-commands-pin.adb b/src/alr/alr-commands-pin.adb index ec1899266..651da4149 100644 --- a/src/alr/alr-commands-pin.adb +++ b/src/alr/alr-commands-pin.adb @@ -181,7 +181,7 @@ package body Alr.Commands.Pin is elsif Cmd.URL.all /= "" then if Cmd.Commit.all /= "" or else Cmd.Branch.all /= "" - or else Alire.URI.Is_HTTP_Or_Git (Cmd.URL.all) + or else Alire.URI.URI_Kind (Cmd.URL.all) in Alire.URI.Git_URIs then -- Pin to remote commit @@ -194,15 +194,30 @@ package body Alr.Commands.Pin is else - -- Pin to dir - - if not Alire.Utils.User_Input.Approve_Dir (Cmd.URL.all) then - Trace.Info ("Abandoned by user."); - return; - end if; - - New_Root.Add_Path_Pin (Crate => Optional_Crate, - Path => Cmd.URL.all); + -- Pin to dir, with a warning if it doesn't look like a path + -- and a subsequent confirmation prompt if it doesn't exist. + + declare + use Alire.URI; + Local : constant Boolean := + URI_Kind (Cmd.URL.all) in Local_URIs; + Path : constant String := + (if Local then Local_Path (Cmd.URL.all) else Cmd.URL.all); + begin + if not Local then + Alire.Put_Warning + ("Assuming '" & Cmd.URL.all & "' is a directory " + & "because no branch or commit was specified."); + end if; + + if not Alire.Utils.User_Input.Approve_Dir (Path) then + Trace.Info ("Abandoned by user."); + return; + end if; + + New_Root.Add_Path_Pin (Crate => Optional_Crate, + Path => Path); + end; end if; diff --git a/src/alr/alr-commands-publish.adb b/src/alr/alr-commands-publish.adb index f94a7369f..9b1d27e65 100644 --- a/src/alr/alr-commands-publish.adb +++ b/src/alr/alr-commands-publish.adb @@ -114,8 +114,8 @@ package body Alr.Commands.Publish is use Alire.Origins; URL : constant String := Args (1); begin - if URI.Scheme (URL) in URI.File_Schemes then - if Archive_Format (URI.Local_Path (URL)) /= Unknown then + if URI.URI_Kind (URL) in URI.Bare_Path then + if Archive_Format (URL) /= Unknown then -- This is a local tarball posing as a remote. Will fail -- unless forced. Alire.Publish.Remote_Origin (URL => URL, diff --git a/src/alr/alr-commands-withing.adb b/src/alr/alr-commands-withing.adb index bd500daec..cfa157a24 100644 --- a/src/alr/alr-commands-withing.adb +++ b/src/alr/alr-commands-withing.adb @@ -10,6 +10,7 @@ with Alire.Releases; with Alire.Roots.Editable; with Alire.Solutions; with Alire.URI; +with Alire.Utils.User_Input; with Alr.OS_Lib; @@ -196,7 +197,7 @@ package body Alr.Commands.Withing is -- Now, add the pin to the path/remote if Cmd.Commit.all /= "" or else Cmd.Branch.all /= "" - or else Alire.URI.Is_HTTP_Or_Git (Cmd.URL.all) + or else Alire.URI.URI_Kind (Cmd.URL.all) in Alire.URI.Git_URIs then -- Pin to remote repo, with optional dependency first @@ -209,11 +210,28 @@ package body Alr.Commands.Withing is else - -- Pin to local folder + -- Pin to local folder, with a warning if it doesn't look like a path + -- and a subsequent confirmation prompt if it doesn't exist. - Root.Add_Path_Pin - (Crate => Crate, - Path => Cmd.URL.all); + declare + use Alire.URI; + Local : constant Boolean := URI_Kind (Cmd.URL.all) in Local_URIs; + Path : constant String := (if Local then Local_Path (Cmd.URL.all) + else Cmd.URL.all); + begin + if not Local then + Alire.Put_Warning + ("Assuming '" & Cmd.URL.all & "' is a directory because no " + & "branch or commit was specified."); + end if; + + if not Alire.Utils.User_Input.Approve_Dir (Path) then + Trace.Info ("Abandoned by user."); + return; + end if; + + Root.Add_Path_Pin (Crate => Crate, Path => Path); + end; end if; end Add_With_Pin; diff --git a/testsuite/fixtures/ambiguous_ssh_origin_index/index.toml b/testsuite/fixtures/ambiguous_ssh_origin_index/index.toml new file mode 100644 index 000000000..bad265e4f --- /dev/null +++ b/testsuite/fixtures/ambiguous_ssh_origin_index/index.toml @@ -0,0 +1 @@ +version = "1.1" diff --git a/testsuite/fixtures/ambiguous_ssh_origin_index/li/libfoo/libfoo-1.0.0.toml b/testsuite/fixtures/ambiguous_ssh_origin_index/li/libfoo/libfoo-1.0.0.toml new file mode 100644 index 000000000..384a76c6b --- /dev/null +++ b/testsuite/fixtures/ambiguous_ssh_origin_index/li/libfoo/libfoo-1.0.0.toml @@ -0,0 +1,9 @@ +description = "Custom repo to check retrieval of unidentified ssh:// crates" +name = "libfoo" +version = "1.0.0" +maintainers = ["some@one.com"] +maintainers-logins = ["mylogin"] + +[origin] +url = "ssh://host.invalid/path/to/repo_without_suffix" +commit = "0000000000000000000000000000000000000000" diff --git a/testsuite/fixtures/ssh_origin_index/index.toml b/testsuite/fixtures/ssh_origin_index/index.toml new file mode 100644 index 000000000..bad265e4f --- /dev/null +++ b/testsuite/fixtures/ssh_origin_index/index.toml @@ -0,0 +1 @@ +version = "1.1" diff --git a/testsuite/fixtures/ssh_origin_index/li/libbar/libbar-1.0.0.toml b/testsuite/fixtures/ssh_origin_index/li/libbar/libbar-1.0.0.toml new file mode 100644 index 000000000..61524e572 --- /dev/null +++ b/testsuite/fixtures/ssh_origin_index/li/libbar/libbar-1.0.0.toml @@ -0,0 +1,9 @@ +description = "Custom repo to check retrieval of git+ssh:// git crates" +name = "libbar" +version = "1.0.0" +maintainers = ["some@one.com"] +maintainers-logins = ["mylogin"] + +[origin] +url = "git+ssh://host.invalid/path/to/repo" +commit = "0000000000000000000000000000000000000000" diff --git a/testsuite/fixtures/ssh_origin_index/li/libbaz/libbaz-1.0.0.toml b/testsuite/fixtures/ssh_origin_index/li/libbaz/libbaz-1.0.0.toml new file mode 100644 index 000000000..71bf93922 --- /dev/null +++ b/testsuite/fixtures/ssh_origin_index/li/libbaz/libbaz-1.0.0.toml @@ -0,0 +1,9 @@ +description = "Custom repo to check retrieval of ssh:// git crates" +name = "libbaz" +version = "1.0.0" +maintainers = ["some@one.com"] +maintainers-logins = ["mylogin"] + +[origin] +url = "ssh://host.invalid/path/to/repo.git/" +commit = "0000000000000000000000000000000000000000" diff --git a/testsuite/fixtures/ssh_origin_index/li/libfoo/libfoo-1.0.0.toml b/testsuite/fixtures/ssh_origin_index/li/libfoo/libfoo-1.0.0.toml new file mode 100644 index 000000000..32278f6e2 --- /dev/null +++ b/testsuite/fixtures/ssh_origin_index/li/libfoo/libfoo-1.0.0.toml @@ -0,0 +1,9 @@ +description = "Custom repo to check retrieval of ssh:// git crates" +name = "libfoo" +version = "1.0.0" +maintainers = ["some@one.com"] +maintainers-logins = ["mylogin"] + +[origin] +url = "ssh://host.invalid/path/to/repo.git" +commit = "0000000000000000000000000000000000000000" diff --git a/testsuite/tests/clean/cache-files/test.py b/testsuite/tests/clean/cache-files/test.py new file mode 100644 index 000000000..8e0eb8997 --- /dev/null +++ b/testsuite/tests/clean/cache-files/test.py @@ -0,0 +1,39 @@ +""" +Verify that `alr clean --cache` works properly +""" + +import os + +from drivers.alr import run_alr, init_local_crate, alr_pin +from drivers.asserts import assert_file_exists +from drivers.helpers import init_git_repo + + +# `alr init` a couple of crates to use as dependencies. +init_local_crate("yyy", enter=False) +yyy_path = os.path.join(os.getcwd(), "yyy") +yyy_url = "git+file:" + yyy_path +init_git_repo("yyy") +init_local_crate("zzz", enter=False) +zzz_path = os.path.join(os.getcwd(), "zzz") + +# `alr init` a new crate and add various dependencies (a plain dependency, +# a git dependency, a plain pin and a git pin). +init_local_crate() +run_alr("with", "hello") +run_alr("with", "libfoo") +alr_pin("yyy", url=yyy_url) +alr_pin("zzz", path=zzz_path) + +# Verify the 'alire/cache' directory now exists and contains a 'pins' directory +# (also 'dependencies' when running in sandboxed mode). +assert_file_exists(os.path.join("alire", "cache", "pins")) + +# Run the 'alr clean --cache' command. +run_alr("clean", "--cache") + +# Verify that the cache directory no longer exists +assert_file_exists(os.path.join("alire", "cache"), wanted=False) + + +print('SUCCESS') diff --git a/testsuite/tests/clean/cache-files/test.yaml b/testsuite/tests/clean/cache-files/test.yaml new file mode 100644 index 000000000..b7535b9da --- /dev/null +++ b/testsuite/tests/clean/cache-files/test.yaml @@ -0,0 +1,4 @@ +driver: python-script +indexes: + basic_index: {} + git_index: {} diff --git a/testsuite/tests/index/git-local/my_crates/hello/hello.gpr b/testsuite/tests/index/git-local/my_crates/hello/hello.gpr new file mode 100644 index 000000000..15cb23051 --- /dev/null +++ b/testsuite/tests/index/git-local/my_crates/hello/hello.gpr @@ -0,0 +1,10 @@ +with "libhello.gpr"; + +project Hello is + + for Source_Dirs use ("src"); + for Object_Dir use "obj"; + for Main use ("hello.adb"); + +end Hello; + diff --git a/testsuite/tests/index/git-local/my_crates/hello/src/hello.adb b/testsuite/tests/index/git-local/my_crates/hello/src/hello.adb new file mode 100644 index 000000000..6236aa7e1 --- /dev/null +++ b/testsuite/tests/index/git-local/my_crates/hello/src/hello.adb @@ -0,0 +1,7 @@ +with Libhello; + +procedure Hello is + +begin + Libhello.Hello_World; +end Hello; diff --git a/testsuite/tests/index/git-local/my_crates/libhello/libhello.gpr b/testsuite/tests/index/git-local/my_crates/libhello/libhello.gpr new file mode 100644 index 000000000..c6a9c4c39 --- /dev/null +++ b/testsuite/tests/index/git-local/my_crates/libhello/libhello.gpr @@ -0,0 +1,10 @@ +project Libhello is + + for Source_Dirs use ("src"); + for Object_Dir use "obj"; + + for Library_Name use "hello"; + for Library_Dir use "lib"; + +end Libhello; + diff --git a/testsuite/tests/index/git-local/my_crates/libhello/src/libhello.adb b/testsuite/tests/index/git-local/my_crates/libhello/src/libhello.adb new file mode 100644 index 000000000..60cb127eb --- /dev/null +++ b/testsuite/tests/index/git-local/my_crates/libhello/src/libhello.adb @@ -0,0 +1,15 @@ +with Ada.Text_IO; + +package body libhello is + + ----------------- + -- Hello_World -- + ----------------- + + procedure Hello_World is + use Ada.Text_IO; + begin + Put_Line ("Hello, world!"); + end Hello_World; + +end libhello; diff --git a/testsuite/tests/index/git-local/my_crates/libhello/src/libhello.ads b/testsuite/tests/index/git-local/my_crates/libhello/src/libhello.ads new file mode 100644 index 000000000..231e491e1 --- /dev/null +++ b/testsuite/tests/index/git-local/my_crates/libhello/src/libhello.ads @@ -0,0 +1,5 @@ +package libhello is + + procedure Hello_World; + +end libhello; diff --git a/testsuite/tests/index/git-local/my_index/index/he/hello/hello-1.0.1.toml b/testsuite/tests/index/git-local/my_index/index/he/hello/hello-1.0.1.toml new file mode 100644 index 000000000..f44dd4abd --- /dev/null +++ b/testsuite/tests/index/git-local/my_index/index/he/hello/hello-1.0.1.toml @@ -0,0 +1,11 @@ +description = "\"Hello, world!\" demonstration project" +name = "hello" +version = "1.0.1" +maintainers = ["alejandro@mosteo.com", "bob@example.com"] +maintainers-logins = ["mylogin"] + +[[depends-on]] +libhello = "^1.0" + +[origin] +# URL omitted because it will be added at the start of the test script diff --git a/testsuite/tests/index/git-local/my_index/index/index.toml b/testsuite/tests/index/git-local/my_index/index/index.toml new file mode 100644 index 000000000..bad265e4f --- /dev/null +++ b/testsuite/tests/index/git-local/my_index/index/index.toml @@ -0,0 +1 @@ +version = "1.1" diff --git a/testsuite/tests/index/git-local/my_index/index/li/libhello/libhello-1.0.0.toml b/testsuite/tests/index/git-local/my_index/index/li/libhello/libhello-1.0.0.toml new file mode 100644 index 000000000..66fcb04d5 --- /dev/null +++ b/testsuite/tests/index/git-local/my_index/index/li/libhello/libhello-1.0.0.toml @@ -0,0 +1,8 @@ +description = "\"Hello, world!\" demonstration project support library" +name = "libhello" +version = "1.0.0" +maintainers = ["alejandro@mosteo.com"] +maintainers-logins = ["mylogin"] + +[origin] +# URL omitted because it will be added at the start of the test script diff --git a/testsuite/tests/index/git-local/test.py b/testsuite/tests/index/git-local/test.py new file mode 100644 index 000000000..67c403f74 --- /dev/null +++ b/testsuite/tests/index/git-local/test.py @@ -0,0 +1,218 @@ +""" +Check that local directories and local git repos can be used as indexes. +""" + +import os +import re +import shutil +import subprocess + +from drivers.alr import run_alr, crate_dirname +from drivers.helpers import init_git_repo, git_branch +from drivers.asserts import assert_eq, assert_match + + +TEST_ROOT_DIR = os.getcwd() +MY_INDEX_PATH = os.path.join(TEST_ROOT_DIR, 'my_index') + + +def run(*args, **kwargs): + sp = subprocess.run(*args, **kwargs) + sp.check_returncode() + return sp + +def check_index_is_configured(name, url, path): + # On MacOS, /var/ is a symlink to /private/var/. Since the full pattern + # below prepends a ".*", "/var/" will match both. + if path.startswith("/private/var"): + path = path.removeprefix("/private") + assert_match( + rf".*\d+.*{re.escape(name)}.*{re.escape(url)}.*{re.escape(path)}", + run_alr("index", "--list").out + ) + +def check_index_works(): + """ + Use the currently configured index to get, build and run the 'hello' crate, + asserting success. + """ + run_alr("get", "hello") + deploy_dir = crate_dirname("hello") + os.chdir(deploy_dir) + assert_eq("Hello, world!\n", run_alr("run").out) + os.chdir("..") + shutil.rmtree(deploy_dir) + + +# Add all necessary paths to the index. We do this now because they need to be +# absolute paths; relative paths don't work for git repo indexes. Note that TOML +# files require backslashes (in windows paths) to be escaped. +my_crates_path = os.path.join(TEST_ROOT_DIR, "my_crates") +hello_manifest_path = os.path.join( + "my_index", "index", "he", "hello", "hello-1.0.1.toml" +) +libhello_manifest_path = os.path.join( + "my_index", "index", "li", "libhello", "libhello-1.0.0.toml" +) +hello_path = os.path.join(my_crates_path, "hello").replace("\\", "\\\\") +libhello_path = os.path.join(my_crates_path, "libhello").replace("\\", "\\\\") +with open(hello_manifest_path, "a") as f: + f.write(f'url = "file:{hello_path}"\n') +with open(libhello_manifest_path, "a") as f: + f.write(f'url = "file:{libhello_path}"\n') + + +# Test adding my_index as a simple directory index. +run_alr("index", "--name", "my_index", "--add", "my_index") +check_index_is_configured("my_index", f"file:{MY_INDEX_PATH}", MY_INDEX_PATH) +# Verify this hasn't created an unnecessary copy in the alr-config directory +# (only an index.toml file which redirects to the existing copy). +index_copy_path = os.path.join("alr-config", "indexes", "my_index") +assert_eq(["index.toml"], os.listdir(index_copy_path)) +# Check that the index can be used to get and build a crate. +check_index_works() +# Clean up for next test case. +run_alr("index", "--del", "my_index") + +# Verify same result with --arg=value form. +run_alr("index", "--name=my_index", "--add=my_index") +check_index_is_configured("my_index", f"file:{MY_INDEX_PATH}", MY_INDEX_PATH) +assert_eq(["index.toml"], os.listdir(index_copy_path)) +check_index_works() +run_alr("index", "--del", "my_index") + +# Verify the same result with a `file:` URL equivalent. +run_alr("index", "--name", "my_index", "--add", "file:my_index") +check_index_is_configured("my_index", f"file:{MY_INDEX_PATH}", MY_INDEX_PATH) +assert_eq(["index.toml"], os.listdir(index_copy_path)) +check_index_works() +run_alr("index", "--del", "my_index") + +# Verify that a `git+file:` URL fails. +p = run_alr( + "index", "--name", "my_index", "--add", "git+file:my_index", + complain_on_error=False +) +assert_match(r'.*Command \["git", "clone", ".*"\] exited with code 128', p.out) + + +# Initialize a normal git repo in the my_index directory. +init_git_repo("my_index") +default_branch = git_branch("my_index") + +# Verify that the simple directory cases above still work. +run_alr("index", "--name", "my_index", "--add", "my_index") +check_index_is_configured("my_index", f"file:{MY_INDEX_PATH}", MY_INDEX_PATH) +check_index_works() +run_alr("index", "--del", "my_index") +run_alr("index", "--name", "my_index", "--add", "file:my_index") +check_index_is_configured("my_index", f"file:{MY_INDEX_PATH}", MY_INDEX_PATH) +check_index_works() +run_alr("index", "--del", "my_index") + +# Verify that a `git+file:` URL now works, making a clone under alr-config with +# the path (not a `file:` URL) as its remote. +run_alr("index", "--name", "my_index", "--add", "git+file:my_index") +check_index_is_configured( + "my_index", + f"git+file:{MY_INDEX_PATH}", + os.path.join(TEST_ROOT_DIR, "alr-config", "indexes", "my_index", "repo") +) +os.chdir(os.path.join("alr-config", "indexes", "my_index", "repo")) +sp = run(["git", "remote", "show", "origin"], capture_output=True) +assert_match(r".*Fetch URL: (?!(git\+)?file:).*my_index", sp.stdout.decode()) +os.chdir(TEST_ROOT_DIR) +check_index_works() +run_alr("index", "--del", "my_index") + + +# Make a bare repo clone of the index. +run(["git", "clone", "--bare", "my_index", "bare_repo_index"]) + +# Verify that this clone can't be used as a simple directory index. +p = run_alr( + "index", "--name", "bare_repo_index", "--add", "bare_repo_index", + complain_on_error=False +) +assert_match( + ".*No index version metadata found in .*bare_repo_index", + p.out +) +p = run_alr( + "index", "--name", "bare_repo_index", "--add", "file:bare_repo_index", + complain_on_error=False +) +assert_match( + ".*No index version metadata found in .*bare_repo_index", + p.out +) + +# Verify that it does work with a `git+file` URL +run_alr( + "index", "--name", "bare_repo_index", "--add", "git+file:bare_repo_index" +) +check_index_is_configured( + "bare_repo_index", + f"git+file:{os.path.join(TEST_ROOT_DIR, 'bare_repo_index')}", + os.path.join(TEST_ROOT_DIR, "alr-config", "indexes", "bare_repo_index", "repo") +) +os.chdir(os.path.join("alr-config", "indexes", "bare_repo_index", "repo")) +sp = run(["git", "remote", "show", "origin"], capture_output=True) +assert_match( + r".*Fetch URL: (?!(git\+)?file:).*bare_repo_index", + sp.stdout.decode() +) +os.chdir(TEST_ROOT_DIR) +check_index_works() +run_alr("index", "--del", "bare_repo_index") + + +# Add a branch to my_index at the current commit, then change the default branch +# so it is missing the libhello crate. +os.chdir(os.path.join("my_index", "index")) +run(["git", "branch", "other_branch"]) +run(["git", "checkout", default_branch]) +shutil.rmtree("li") +run(["git", "add", "*"]) +run(["git", "commit", "-m", "Remove libhello"]) +os.chdir(TEST_ROOT_DIR) + +# Verify that `alr index add` adds the checked out branch (so `alr get hello` +# will fail). +run_alr("index", "--name", "my_index", "--add", "git+file:my_index") +check_index_is_configured( + "my_index", + f"git+file:{MY_INDEX_PATH}", + os.path.join(TEST_ROOT_DIR, "alr-config", "indexes", "my_index", "repo") +) +p = run_alr("get", "hello", quiet=False, complain_on_error=False) +assert_match(r".*Could not find a complete solution for hello=1\.0\.1", p.out) +# Verify this doesn't change if the checked out branch is changed subsequent to +# the `alr index add` +os.chdir("my_index") +run(["git", "checkout", "other_branch"]) +os.chdir(TEST_ROOT_DIR) +run_alr("index", "--update-all") +p = run_alr("get", "hello", quiet=False, complain_on_error=False) +assert_match(r".*Could not find a complete solution for hello=1\.0\.1", p.out) +run_alr("index", "--del", "my_index") + +# Now that other_branch is checked out, verify that `alr index add` adds the +# version with libhello still present, and therefore `alr get hello` succeeds. +run_alr("index", "--name", "my_index", "--add", "git+file:my_index") +check_index_is_configured( + "my_index", + f"git+file:{MY_INDEX_PATH}", + os.path.join(TEST_ROOT_DIR, "alr-config", "indexes", "my_index", "repo") +) +check_index_works() +# Again, check that checkout commands after `alr index add` have no effect +os.chdir("my_index") +run(["git", "checkout", default_branch]) +os.chdir(TEST_ROOT_DIR) +run_alr("index", "--update-all") +check_index_works() +run_alr("index", "--del", "my_index") + + +print("SUCCESS") diff --git a/testsuite/tests/index/git-local/test.yaml b/testsuite/tests/index/git-local/test.yaml new file mode 100644 index 000000000..fa855459b --- /dev/null +++ b/testsuite/tests/index/git-local/test.yaml @@ -0,0 +1,3 @@ +driver: python-script +indexes: + compiler_only_index: {} diff --git a/testsuite/tests/index/git-ssh-remote/test.py b/testsuite/tests/index/git-ssh-remote/test.py index d442c3f2b..5ee5f4ea3 100644 --- a/testsuite/tests/index/git-ssh-remote/test.py +++ b/testsuite/tests/index/git-ssh-remote/test.py @@ -7,7 +7,7 @@ SSH_EXPLICIT_INDEX = f"git+ssh://{SSH_IMPLICIT_INDEX}" # Test that we can add an index using implicit ssh -run_alr("index", "--name", "implicit", "--add", SSH_EXPLICIT_INDEX) +run_alr("index", "--name", "implicit", "--add", SSH_IMPLICIT_INDEX) run_alr("index", "--check") run_alr("index", "--update-all") # Check pulling diff --git a/testsuite/tests/index/local-index-not-found/test.py b/testsuite/tests/index/local-index-not-found/test.py index 4add3de68..c30f09077 100644 --- a/testsuite/tests/index/local-index-not-found/test.py +++ b/testsuite/tests/index/local-index-not-found/test.py @@ -10,26 +10,47 @@ from drivers.alr import prepare_indexes, run_alr from drivers.asserts import assert_match +from drivers.helpers import replace_in_file + + +INDEX_DIR = "no-such-directory" +REL_CONF_PATH = os.path.join('alr-config', 'indexes', 'bad_index', 'index.toml') +ERR_MSG = ( + f'.*ERROR: Cannot load metadata from .*{re.escape(REL_CONF_PATH)}: ' + f'Not a readable directory: {re.escape(os.path.join(".", INDEX_DIR))}\n' +) + + +# Directly configure the non-existent index in Alire's config directory +prepare_indexes( + 'alr-config', '.', {'bad_index': {'dir': INDEX_DIR, 'in_fixtures': False}} +) +# Verify that `alr search` gives a suitable error +p = run_alr("search", "--crates", complain_on_error=False, debug=False) +assert_match(ERR_MSG, p.out) + +# Rewrite the url field of the config 'index.toml' file to use a 'file://' URL +replace_in_file(REL_CONF_PATH, "url = '", "url = 'file://") +# Verify this yields the same result +p = run_alr("search", "--crates", complain_on_error=False, debug=False) +assert_match(ERR_MSG, p.out) + +# Repeat both cases, but using the `alr index --add` UI +p = run_alr( + "index", "--add", "no-such-directory", "--name", "bad_index", + complain_on_error=False +) +assert_match(ERR_MSG, p.out) +p = run_alr( + "index", "--add", "file:no-such-directory", "--name", "bad_index", + complain_on_error=False +) +assert_match(ERR_MSG, p.out) +p = run_alr( + "index", "--add", "file://no-such-directory", "--name", "bad_index", + complain_on_error=False +) +assert_match(ERR_MSG, p.out) -for d in ('no-such-directory', - 'file://no-such-directory', ): - - # Delete old configuration and indexes, but disable msys2 installation or - # installation will be reattempted. - rm('alr-config', recursive=True) - run_alr("settings", "--global", "--set", "msys2.do_not_install", "true") - - prepare_indexes('alr-config', '.', - {'bad_index': {'dir': d, 'in_fixtures': False}}) - p = run_alr("search", "--crates", complain_on_error=False, debug=False) - - path_excerpt = os.path.join('alr-config', 'indexes', 'bad_index', - 'index.toml') - separator = re.escape(os.path.sep) - assert_match('ERROR: Cannot load metadata from .*{}:' - ' Not a readable directory: .{}{}' - '\n' - .format(re.escape(path_excerpt), separator, d), - p.out) print('SUCCESS') diff --git a/testsuite/tests/index/local-index-not-found/test.yaml b/testsuite/tests/index/local-index-not-found/test.yaml index 847671162..32c747b3f 100644 --- a/testsuite/tests/index/local-index-not-found/test.yaml +++ b/testsuite/tests/index/local-index-not-found/test.yaml @@ -1,5 +1 @@ driver: python-script -indexes: - bad_index: - dir: file://no-such-directory - in_fixtures: false diff --git a/testsuite/tests/pin/bad-path/test.py b/testsuite/tests/pin/bad-path/test.py index 2194f9f2f..0eaf3ac7e 100644 --- a/testsuite/tests/pin/bad-path/test.py +++ b/testsuite/tests/pin/bad-path/test.py @@ -2,15 +2,57 @@ Verify that pinning a bad path is rejected """ -from drivers.alr import run_alr, alr_pin, init_local_crate -from drivers.asserts import assert_eq, assert_match +from drivers.alr import run_alr, alr_pin, alr_unpin, init_local_crate +from drivers.asserts import assert_match +# Pin a local path which doesn't exist init_local_crate() alr_pin("badcrate", path="../bad/path", update=False) # Now the update should detect the bad path p = run_alr("update", complain_on_error=False) -assert_match(".*Pin path is not a valid directory:.*", - p.out) +assert_match( + ".*Pin path is not a valid directory:.*", + p.out +) + +# Verify the same outcome with the command line interface +alr_unpin("badcrate") +p = run_alr("pin", "badcrate", "--use", "../bad/path") +assert_match( + r".*Given path does not exist: \.\./bad/path", + p.out +) +# Note that `alr` returns zero because the confirmation prompt abandons the +# operation by default. +assert_match( + r".*Do you want to continue anyway\?", + p.out +) +assert_match( + r".*Using default: No", + p.out +) + +# Verify the same result with a `file:` URL. +p = run_alr("pin", "badcrate", "--use", "file:../bad/path") +assert_match( + r".*Given path does not exist: \.\./bad/path", + p.out +) + +# Verify that a warning is issued for a path that looks like a remote URL. +p = run_alr("pin", "badcrate", "--use", "https://some.host/path", quiet=False) +assert_match( + ( + r".*Assuming 'https://some\.host/path' is a directory because no " + r"branch or commit was specified\." + ), + p.out +) +assert_match( + r".*Given path does not exist: https://some\.host/path", + p.out +) print('SUCCESS') diff --git a/testsuite/tests/pin/branch-remote-protocols/test.py b/testsuite/tests/pin/branch-remote-protocols/test.py index 702bd22b3..97d2fee25 100644 --- a/testsuite/tests/pin/branch-remote-protocols/test.py +++ b/testsuite/tests/pin/branch-remote-protocols/test.py @@ -1,5 +1,5 @@ """ -Check pinning to branches with "git+ssh://" and "xyz+https://" urls +Check pinning to branches with "ssh://", "git+ssh://" and "git+https://" urls """ import os @@ -35,11 +35,23 @@ # Perform the actual tests urls = [ "git+ssh://ssh.gitlab.company-name.com/path/to/repo.git", - "xyz+https://github.com/path/to/repo.git", + "git+ssh://ssh.gitlab.company-name.com/path/to/repo", + "git+https://github.com/path/to/repo.git", + # Should recognize URLs with ".git" suffix (without "git+" prefix) + "ssh://ssh.gitlab.company-name.com/path/to/repo.git", + "ssh://ssh.gitlab.company-name.com/path/to/repo.git/", + "https://some.host/path/to/repo.git", + # Should recognize github.com even without ".git" or "git+" + "https://github.com/path/to/repo", ] sanitised_urls = [ "ssh://ssh.gitlab.company-name.com/path/to/repo.git", + "ssh://ssh.gitlab.company-name.com/path/to/repo", "https://github.com/path/to/repo.git", + "ssh://ssh.gitlab.company-name.com/path/to/repo.git", + "ssh://ssh.gitlab.company-name.com/path/to/repo.git/", + "https://some.host/path/to/repo.git", + "https://github.com/path/to/repo", ] cache_test_file_path = "alire/cache/pins/remote/test_file" mocked_git_dir = os.path.join(os.getcwd(), "mock_path") diff --git a/testsuite/tests/pin/equivalent/test.py b/testsuite/tests/pin/equivalent/test.py index 0c74e2cf3..d18e93d77 100644 --- a/testsuite/tests/pin/equivalent/test.py +++ b/testsuite/tests/pin/equivalent/test.py @@ -2,13 +2,14 @@ Verify that using manual edition and `alr pin` result in equivalent outputs """ +import os + +from e3.fs import rm + from drivers.alr import run_alr, alr_pin, init_local_crate from drivers.asserts import assert_eq from drivers.helpers import init_git_repo, git_branch -import os -import shutil - def check_equivalent(crate, path="", url="", commit="", branch=""): """ @@ -31,18 +32,18 @@ def check_equivalent(crate, path="", url="", commit="", branch=""): if i == 1: assert_eq(p[0], p[1]) - # Cleanup + # Cleanup (use e3.fs, because shutil.rmtree() fails on links on Windows) os.chdir("..") - shutil.rmtree("xxx") + rm("xxx", recursive=True) # Local pinnable crate init_local_crate("yyy", enter=False) -yyy_path = "../yyy" +yyy_path = os.path.join(os.getcwd(), "yyy") # Local pinnable raw project os.mkdir("zzz") -zzz_path = "../zzz" +zzz_path = os.path.join(os.getcwd(), "zzz") # Simple pin, no restrictions check_equivalent("yyy", path=yyy_path) @@ -51,8 +52,7 @@ def check_equivalent(crate, path="", url="", commit="", branch=""): # Prepare repository head = init_git_repo("yyy") branch = git_branch("yyy") -os.rename("yyy", "yyy.git") # to be recognizable as a git url -url = "../yyy.git" +url = "git+file:" + yyy_path # to be recognizable as a git url # Simple git remote, explicit crate check_equivalent(crate="yyy", url=url) diff --git a/testsuite/tests/pin/remote/test.py b/testsuite/tests/pin/remote/test.py index f48d40ca5..f51a8adbc 100644 --- a/testsuite/tests/pin/remote/test.py +++ b/testsuite/tests/pin/remote/test.py @@ -10,6 +10,10 @@ from drivers.asserts import assert_eq +# Ensure the "remote" looks like a git repo +URL = "git+file:" + os.path.join(os.getcwd(), "upstream") + + def verify(head=""): # Either head or branch /= "" # Check that the linked dir exists at the expected location pin_path = (f"alire/cache/pins/upstream" + @@ -18,7 +22,7 @@ def verify(head=""): # Either head or branch /= "" # Verify info reported by alr p = run_alr("pin") - assert_eq(f"upstream file:{pin_path} ../upstream.git" + + assert_eq(f"upstream file:{pin_path} {URL}" + ("" if head == "" else f"#{head[0:8]}") + "\n", p.out) @@ -43,27 +47,26 @@ def verify(head=""): # Either head or branch /= "" init_local_crate(name="upstream", binary=False) head = init_git_repo(".") os.chdir("..") -os.rename("upstream", "upstream.git") # so it is recognized as git repo # Initialize a client crate that will use the remote init_local_crate() # This leaves us inside the new crate # Add using with directly -run_alr("with", "--use", "../upstream.git", "--commit", head) +run_alr("with", "--use", URL, "--commit", head) verify(head) # Add using with, without head commit -run_alr("with", "--use", "../upstream.git") +run_alr("with", "--use", URL) verify() # Pin afterwards, with commit run_alr("with", "upstream", force=True) # force, as it is unsolvable -run_alr("pin", "upstream", "--use", "../upstream.git", "--commit", head) +run_alr("pin", "upstream", "--use", URL, "--commit", head) verify(head) # Pin afterwards, without commit run_alr("with", "upstream", force=True) -run_alr("pin", "upstream", "--use", "../upstream.git") +run_alr("pin", "upstream", "--use", URL) verify() print('SUCCESS') diff --git a/testsuite/tests/publish/bad-arguments/test.py b/testsuite/tests/publish/bad-arguments/test.py index 723013038..7c71044a0 100644 --- a/testsuite/tests/publish/bad-arguments/test.py +++ b/testsuite/tests/publish/bad-arguments/test.py @@ -20,13 +20,10 @@ # Missing commit for git remotes p = run_alr("publish", "git+http://github.com/repo", complain_on_error=False) -assert_match(".*commit id is mandatory for a VCS origin.*", p.out) - -# Detect github case and give more precise error. This serves also to check -# that github remotes without leading git+ or trailing .git are accepted. -p = run_alr("publish", "https://github.com/missingext", - complain_on_error=False) -assert_match(".*commit id is mandatory for a VCS origin.*", p.out) +assert_match( + ".*URL seems to point to a repository, but no commit was provided.*", + p.out +) # Bad commit length p = run_alr("publish", "git+http://github.com/repo", "deadbeef", @@ -44,6 +41,26 @@ assert_match(".*invalid mercurial commit id, 40 digits hexadecimal expected.*", p.out) +# Check that github remotes without leading git+ or trailing .git are treated +# as source archives (with a warning) if no commit is provided and repositories +# otherwise. +p = run_alr("publish", "https://github.com/missingext", + quiet=False, complain_on_error=False) +assert_match( + ".*Assuming origin is a source archive because no commit was provided", + p.out +) +assert_match( + ".*Unable to determine archive format from file extension.*", + p.out +) +p = run_alr("publish", "https://github.com/missingext", "deadbeef", + complain_on_error=False) +assert_match( + ".*invalid git commit id, 40 digits hexadecimal expected.*", + p.out +) + # VCS without transport or extension p = run_alr("publish", "http://somehost.com/badrepo", "deadbeef", complain_on_error=False) diff --git a/testsuite/tests/publish/check-trusted/test.py b/testsuite/tests/publish/check-trusted/test.py index 2159af1b5..97e2e4aad 100644 --- a/testsuite/tests/publish/check-trusted/test.py +++ b/testsuite/tests/publish/check-trusted/test.py @@ -14,12 +14,16 @@ assert_match(f".*Origin is hosted on unknown site: {domain}.*", p.out) # Try that having credentials doesn't interfere with the previous check and -# that the domain was recognized properly +# that the domain was recognized properly. +# +# The presence of credentials means the origin is considered private, so +# we use '--for-private-index' to skip the "The origin cannot use a private +# remote" error. for domain in ["badsite.com", "ggithub.com", "github.comm"]: for creds in ["user", "user:passwd"]: p = run_alr("publish", f"http://{creds}@{domain}/repo.git", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", - "--skip-submit", + "--for-private-index", complain_on_error=False) assert_match(f".*Origin is hosted on unknown site: {domain}.*", p.out) diff --git a/testsuite/tests/publish/file-scheme/test.py b/testsuite/tests/publish/file-scheme/test.py new file mode 100644 index 000000000..74fa794a0 --- /dev/null +++ b/testsuite/tests/publish/file-scheme/test.py @@ -0,0 +1,27 @@ +""" +Check that publish gives appropriate errors for URIs with a "file:" scheme +""" + + +from drivers.alr import run_alr +from drivers.asserts import assert_match + + +# With commit argument +commit = "0"*40 +p = run_alr("publish", "file:/path/to/repo/", commit, complain_on_error=False) +assert_match(r".*unknown VCS URL: file:/path/to/repo/.*", p.out) + +# Without commit argument (path to file) +p = run_alr("publish", "file:/path/to/repo", complain_on_error=False) +assert_match( + r".*Unable to determine archive format from file extension.*", + p.out +) + +# Without commit argument (path to directory) +p = run_alr("publish", "file:/path/to/repo/", complain_on_error=False) +assert_match(r".*Unable to determine archive name: please specify one.*", p.out) + + +print('SUCCESS') diff --git a/testsuite/tests/publish/file-scheme/test.yaml b/testsuite/tests/publish/file-scheme/test.yaml new file mode 100644 index 000000000..32c747b3f --- /dev/null +++ b/testsuite/tests/publish/file-scheme/test.yaml @@ -0,0 +1 @@ +driver: python-script diff --git a/testsuite/tests/publish/private-indexes/test.py b/testsuite/tests/publish/private-indexes/test.py index a21265f43..0079076a3 100644 --- a/testsuite/tests/publish/private-indexes/test.py +++ b/testsuite/tests/publish/private-indexes/test.py @@ -4,6 +4,7 @@ import os +import re import shutil import subprocess @@ -147,7 +148,7 @@ def test( # GitHub account with a fork of the community index. test( args=force_arg + ["publish", "--skip-submit"], - url="https://github.com/some_user/repo-name.git", + url="https://github.com/some_user/repo-name", maint_logins='["github-username"]', num_confirms=2, output=[ @@ -162,13 +163,13 @@ def test( ], gen_manifest=[ # "git+" should be prepended to avoid ambiguity - r'.*url = "git\+https://github\.com/some_user/repo-name\.git".*', + r'.*url = "git\+https://github\.com/some_user/repo-name".*', ], expect_success=True ) test( args=force_arg + ["publish", "--for-private-index"], - url="https://github.com/some_user/repo-name.git", + url="https://github.com/some_user/repo-name", maint_logins='["github-username"]', num_confirms=2, output=[ @@ -178,7 +179,7 @@ def test( r".*Please upload this file to the index in the xx/xxx/ subdirectory", ], gen_manifest=[ - r'.*url = "git\+https://github\.com/some_user/repo-name\.git".*', + r'.*url = "git\+https://github\.com/some_user/repo-name".*', ], expect_success=True ) @@ -186,7 +187,7 @@ def test( # A crate suitable for the community index, with a GitHub user configured: test( args=force_arg + ["publish", "--skip-submit"], - url="https://github.com/some_user/repo-name.git", + url="https://github.com/some_user/repo-name", maint_logins='["github-username"]', github_user="github-username", num_confirms=2, @@ -206,54 +207,60 @@ def test( ), ], gen_manifest=[ - r'.*url = "git\+https://github\.com/some_user/repo-name\.git".*', + r'.*url = "git\+https://github\.com/some_user/repo-name".*', ], expect_success=True ) # A crate unsuitable for the community index because its origin is private: - # - # "alr publish" should fail, because the origin URL looks private (it will - # also fail if the user does not provide a GitHub account with a fork of the - # community index, but that check comes later). - test( - args=force_arg + ["publish"], - url="git@bitbucket.org:/some_user/repo-name.git", - maint_logins='["github-username"]', - num_confirms=1, - output=[ - r".*The remote URL seems to require repository ownership: .*", - ], - gen_manifest=None, - expect_success=False - ) - # "alr publish --skip-submit" will fail for the same reason. - test( - args=force_arg + ["publish", "--skip-submit"], - url="git@bitbucket.org:/some_user/repo-name.git", - maint_logins='["github-username"]', - num_confirms=1, - output=[ - r".*The remote URL seems to require repository ownership: .*", - ], - gen_manifest=None, - expect_success=False - ) - # "alr publish --for-private-index" will succeed. - test( - args=force_arg + ["publish", "--for-private-index"], - url="git@bitbucket.org:/some_user/repo-name.git", - maint_logins='["github-username"]', - num_confirms=2, - output=[ - r".*Success: Your index manifest file has been generated.*", - r".*Please upload this file to the index in the xx/xxx/ subdirectory", - ], - gen_manifest=[ - r'.*url = "git@bitbucket\.org:/some_user/repo-name\.git".*', - ], - expect_success=True - ) + private_urls = [ + "ssh://github.com/some_user/repo-name.git", + "git@bitbucket.org:/some_user/repo-name.git", + "https://user@github.com/some_user/repo-name.git", + "https://user:pass@github.com/some_user/repo-name.git", + ] + for url in private_urls: + # "alr publish" should fail, because the origin URL looks private (it + # will also fail if the user does not provide a GitHub account with a + # fork of the community index, but that check comes later). + test( + args=force_arg + ["publish"], + url=url, + maint_logins='["github-username"]', + num_confirms=1, # (fails before second confirmation) + output=[ + r".*The origin cannot use a private remote:.*", + ], + gen_manifest=None, + expect_success=False + ) + # "alr publish --skip-submit" will fail for the same reason. + test( + args=force_arg + ["publish", "--skip-submit"], + url=url, + maint_logins='["github-username"]', + num_confirms=1, + output=[ + r".*The origin cannot use a private remote:.*", + ], + gen_manifest=None, + expect_success=False + ) + # "alr publish --for-private-index" will succeed. + test( + args=force_arg + ["publish", "--for-private-index"], + url=url, + maint_logins='["github-username"]', + num_confirms=2, + output=[ + r".*Success: Your index manifest file has been generated.*", + r".*Please upload this file to the index in the xx/xxx/ subdirectory", + ], + gen_manifest=[ + f'.*url = "{re.escape(url)}".*', + ], + expect_success=True + ) # A crate unsuitable for the community index because it has a # "maintainers-logins" value which is invalid for GitHub: @@ -261,7 +268,7 @@ def test( # "alr publish" and "alr publish --skip-submit" should fail. test( args=force_arg + ["publish"], - url="https://github.com/some_user/repo-name.git", + url="https://github.com/some_user/repo-name", maint_logins='["valid-for-GitHub", "invalid_for_GitHub"]', num_confirms=0, # (fails before first confirmation) output=[ @@ -275,7 +282,7 @@ def test( ) test( args=force_arg + ["publish", "--skip-submit"], - url="https://github.com/some_user/repo-name.git", + url="https://github.com/some_user/repo-name", maint_logins='["valid-for-GitHub", "invalid_for_GitHub"]', num_confirms=0, output=[ @@ -290,7 +297,7 @@ def test( # "alr publish --for-private-index" will succeed. test( args=force_arg + ["publish", "--for-private-index"], - url="https://github.com/some_user/repo-name.git", + url="https://github.com/some_user/repo-name", maint_logins='["valid-for-GitHub", "invalid_for_GitHub"]', num_confirms=2, output=[ @@ -298,7 +305,7 @@ def test( r".*Please upload this file to the index in the xx/xxx/ subdirectory", ], gen_manifest=[ - r'.*url = "git\+https://github\.com/some_user/repo-name\.git".*', + r'.*url = "git\+https://github\.com/some_user/repo-name".*', ], expect_success=True ) @@ -309,7 +316,7 @@ def test( # "alr publish" and "alr publish --skip-submit" should fail. test( args=force_arg + ["publish"], - url="https://github.com/some_user/repo-name.git", + url="https://github.com/some_user/repo-name", maint_logins=None, num_confirms=0, # (fails before first confirmation) output=[ @@ -320,7 +327,7 @@ def test( ) test( args=force_arg + ["publish", "--skip-submit"], - url="https://github.com/some_user/repo-name.git", + url="https://github.com/some_user/repo-name", maint_logins=None, num_confirms=0, output=[ @@ -332,7 +339,7 @@ def test( # "alr publish --for-private-index" will succeed. test( args=force_arg + ["publish", "--for-private-index"], - url="https://github.com/some_user/repo-name.git", + url="https://github.com/some_user/repo-name", maint_logins=None, num_confirms=2, output=[ @@ -340,7 +347,7 @@ def test( r".*Please upload this file to the index in the xx/xxx/ subdirectory", ], gen_manifest=[ - r'.*url = "git\+https://github\.com/some_user/repo-name\.git".*', + r'.*url = "git\+https://github\.com/some_user/repo-name".*', ], expect_success=True ) @@ -352,7 +359,7 @@ def test( # all. test( args=force_arg + ["publish"], - url="https://github.com/some_user/repo-name.git", + url="https://github.com/some_user/repo-name", maint_logins="[]", num_confirms=0, output=[ @@ -363,7 +370,7 @@ def test( ) test( args=force_arg + ["publish", "--skip-submit"], - url="https://github.com/some_user/repo-name.git", + url="https://github.com/some_user/repo-name", maint_logins="[]", num_confirms=0, output=[ @@ -374,7 +381,7 @@ def test( ) test( args=force_arg + ["publish", "--for-private-index"], - url="https://github.com/some_user/repo-name.git", + url="https://github.com/some_user/repo-name", maint_logins="[]", num_confirms=2, output=[ @@ -382,7 +389,7 @@ def test( r".*Please upload this file to the index in the xx/xxx/ subdirectory", ], gen_manifest=[ - r'.*url = "git\+https://github\.com/some_user/repo-name\.git".*', + r'.*url = "git\+https://github\.com/some_user/repo-name".*', ], expect_success=True ) diff --git a/testsuite/tests/publish/ssh-remote-origin/test.py b/testsuite/tests/publish/ssh-remote-origin/test.py index 3dc33a82a..210fa3f4a 100644 --- a/testsuite/tests/publish/ssh-remote-origin/test.py +++ b/testsuite/tests/publish/ssh-remote-origin/test.py @@ -8,6 +8,8 @@ urls = [ + "git+ssh://host.invalid/path/to/repo.git", + "ssh://host.invalid/path/to/repo.git", "git@host.invalid:/path/to/repo.git", ] commit = "0" * 40 diff --git a/testsuite/tests/with/ambiguous_ssh_origin/test.py b/testsuite/tests/with/ambiguous_ssh_origin/test.py new file mode 100644 index 000000000..ad719a4c1 --- /dev/null +++ b/testsuite/tests/with/ambiguous_ssh_origin/test.py @@ -0,0 +1,23 @@ +""" +Check that ambiguous ssh origins yield a suggestion +""" + + +from drivers.alr import init_local_crate, run_alr +from drivers.asserts import assert_match + + +# This origin has an "ssh://" scheme, but neither a "git+" prefix nor a ".git" +# suffix, so should fail with a prefix suggestion +init_local_crate(update=False) +p = run_alr("with", "libqux", complain_on_error=False) +assert_match( + ( + r".*Pure 'ssh://' URLs are not valid crate origins. You may want " + r"git\+ssh://host.invalid/path/to/repo_without_suffix instead.*" + ), + p.out +) + + +print("SUCCESS") diff --git a/testsuite/tests/with/ambiguous_ssh_origin/test.yaml b/testsuite/tests/with/ambiguous_ssh_origin/test.yaml new file mode 100644 index 000000000..e7365ab9e --- /dev/null +++ b/testsuite/tests/with/ambiguous_ssh_origin/test.yaml @@ -0,0 +1,3 @@ +driver: python-script +indexes: + ambiguous_ssh_origin_index: {} diff --git a/testsuite/tests/with/equivalent/test.py b/testsuite/tests/with/equivalent/test.py index 28cf3f74c..1f3897c43 100644 --- a/testsuite/tests/with/equivalent/test.py +++ b/testsuite/tests/with/equivalent/test.py @@ -2,13 +2,14 @@ Verify that using manual edition and `alr with` result in equivalent manifests """ +import os + +from e3.fs import rm + from drivers.alr import run_alr, alr_with, init_local_crate -from drivers.asserts import assert_eq, assert_match +from drivers.asserts import assert_eq from drivers.helpers import init_git_repo, git_branch -import os -import shutil - def check_equivalent(dep="", path="", url="", commit="", branch=""): """ @@ -31,9 +32,9 @@ def check_equivalent(dep="", path="", url="", commit="", branch=""): if i == 1: assert_eq(p[0], p[1]) - # Cleanup + # Cleanup (use e3.fs, because shutil.rmtree() fails on links on Windows) os.chdir("..") - shutil.rmtree("xxx") + rm("xxx", recursive=True) # Simple with without subset cannot be tested as `alr with` will narrow down @@ -50,13 +51,13 @@ def check_equivalent(dep="", path="", url="", commit="", branch=""): # Pinned folder init_local_crate("yyy", enter=False) -check_equivalent("yyy~0", path="../yyy") +yyy_path = os.path.join(os.getcwd(), "yyy") +check_equivalent("yyy~0", path=yyy_path) # Prepare repository head = init_git_repo("yyy") branch = git_branch("yyy") -os.rename("yyy", "yyy.git") # to be recognizable as a git url -url = "../yyy.git" +url = "git+file:" + yyy_path # to be recognizable as a git url # Simple git remote, explicit crate & version check_equivalent(dep="yyy~0", url=url) diff --git a/testsuite/tests/with/pin-bad-path/test.py b/testsuite/tests/with/pin-bad-path/test.py new file mode 100644 index 000000000..21be3a5a3 --- /dev/null +++ b/testsuite/tests/with/pin-bad-path/test.py @@ -0,0 +1,50 @@ +""" +Verify that pinning a bad path fails +""" + + +from drivers.alr import run_alr, init_local_crate +from drivers.asserts import assert_match + + +# Pin a local path which doesn't exist +init_local_crate() +p = run_alr("with", "badcrate", "--use", "../bad/path") +assert_match( + r".*Given path does not exist: \.\./bad/path", + p.out +) +# Note that `alr` returns zero because the confirmation prompt abandons the +# operation by default. +assert_match( + r".*Do you want to continue anyway\?", + p.out +) +assert_match( + r".*Using default: No", + p.out +) + +# Verify the same result with a `file:` URL. +p = run_alr("with", "badcrate", "--use", "file:../bad/path") +assert_match( + r".*Given path does not exist: \.\./bad/path", + p.out +) + +# Verify that a warning is issued for a path that looks like a remote URL. +p = run_alr("with", "badcrate", "--use", "https://some.host/path", quiet=False) +assert_match( + ( + r".*Assuming 'https://some\.host/path' is a directory because no " + r"branch or commit was specified\." + ), + p.out +) +assert_match( + r".*Given path does not exist: https://some\.host/path", + p.out +) + + +print('SUCCESS') diff --git a/testsuite/tests/with/pin-bad-path/test.yaml b/testsuite/tests/with/pin-bad-path/test.yaml new file mode 100644 index 000000000..32c747b3f --- /dev/null +++ b/testsuite/tests/with/pin-bad-path/test.yaml @@ -0,0 +1 @@ +driver: python-script diff --git a/testsuite/tests/with/ssh-origins/test.py b/testsuite/tests/with/ssh-origins/test.py new file mode 100644 index 000000000..c0e86de79 --- /dev/null +++ b/testsuite/tests/with/ssh-origins/test.py @@ -0,0 +1,25 @@ +""" +Check that 'ssh://' and 'git+ssh://' crate origins are not considered invalid +""" + + +from drivers.alr import init_local_crate, run_alr +from drivers.asserts import assert_match + + +# We expect attempts to get crates from these origins to fail, but it should be +# because the git clone command fails, not because Alire rejects the origin URL. +init_local_crate(update=False) +for lib_name in ["libfoo", "libbar", "libbaz"]: + p = run_alr("with", lib_name, complain_on_error=False) + assert_match( + ( + r'.*Command \["git", "clone", "--recursive", "-q", ' + r'"ssh://host\.invalid/path/to/repo.git", ".*"\] ' + r'exited with code 128' + ), + p.out + ) + + +print("SUCCESS") diff --git a/testsuite/tests/with/ssh-origins/test.yaml b/testsuite/tests/with/ssh-origins/test.yaml new file mode 100644 index 000000000..af97f4ea4 --- /dev/null +++ b/testsuite/tests/with/ssh-origins/test.yaml @@ -0,0 +1,3 @@ +driver: python-script +indexes: + ssh_origin_index: {}