From 28f27cbf4b2c3163a1617a57bb1f64672d759f95 Mon Sep 17 00:00:00 2001 From: Seb M'Caw Date: Thu, 8 Aug 2024 14:22:38 +0000 Subject: [PATCH 1/9] Add support for private indexes to "alr publish" --- doc/catalog-format-spec.md | 10 +- doc/publishing.md | 41 +- src/alire/alire-properties-from_toml.ads | 2 - src/alire/alire-properties-labeled.adb | 8 +- src/alire/alire-publish-submit.adb | 2 +- src/alire/alire-publish.adb | 140 +++++-- src/alire/alire-publish.ads | 14 +- src/alire/alire-releases.ads | 6 + src/alire/alire-toml_index.adb | 9 +- src/alire/alire-toml_index.ads | 5 + .../alire-utils-user_input-query_config.adb | 16 +- .../alire-utils-user_input-query_config.ads | 4 +- src/alr/alr-commands-init.adb | 63 ++- src/alr/alr-commands-publish.adb | 16 +- src/alr/alr-commands-publish.ads | 9 +- testsuite/drivers/alr.py | 72 +++- testsuite/drivers/asserts.py | 8 + testsuite/drivers/helpers.py | 68 +++ .../he/hello_world/hello_world-0.1.0.toml | 2 +- testsuite/tests/index/maint-bad-login/test.py | 4 +- testsuite/tests/init/github-login/test.py | 103 +++++ testsuite/tests/init/github-login/test.yaml | 1 + testsuite/tests/monorepo/basic/test.py | 2 +- .../tests/monorepo/doubly-nested/test.py | 4 +- .../tests/monorepo/manifest-in-place/test.py | 2 +- testsuite/tests/monorepo/multi-commit/test.py | 4 +- .../tests/monorepo/subdir-in-tar/test.py | 2 +- .../tests/pin/branch-remote-protocols/test.py | 78 ++-- .../publish/check-pre-release-version/test.py | 2 +- .../tests/publish/local-repo-branched/test.py | 11 +- .../tests/publish/local-repo-nonstd/test.py | 2 +- testsuite/tests/publish/local-repo/test.py | 2 +- testsuite/tests/publish/pin-removal/test.py | 2 +- .../my_index/crates/crate/alire.toml | 9 + .../my_index/crates/crate/crate.gpr | 22 + .../my_index/crates/crate/src/crate.adb | 4 + .../my_index/index/cr/crate/crate-1.0.0.toml | 13 + .../private-indexes/my_index/index/index.toml | 1 + .../tests/publish/private-indexes/test.py | 389 ++++++++++++++++++ .../tests/publish/private-indexes/test.yaml | 4 + .../publish/remote-origin-nonstd/test.py | 4 +- testsuite/tests/publish/remote-origin/test.py | 4 +- .../tests/publish/ssh-remote-origin/test.py | 34 ++ .../tests/publish/ssh-remote-origin/test.yaml | 1 + .../publish/tarball-plaindir-nonstd/test.py | 2 +- .../tests/publish/tarball-plaindir/test.py | 2 +- .../tests/publish/tarball-repo-nonstd/test.py | 14 +- testsuite/tests/publish/tarball-repo/test.py | 14 +- 48 files changed, 1038 insertions(+), 193 deletions(-) create mode 100644 testsuite/tests/init/github-login/test.py create mode 100644 testsuite/tests/init/github-login/test.yaml create mode 100644 testsuite/tests/publish/private-indexes/my_index/crates/crate/alire.toml create mode 100644 testsuite/tests/publish/private-indexes/my_index/crates/crate/crate.gpr create mode 100644 testsuite/tests/publish/private-indexes/my_index/crates/crate/src/crate.adb create mode 100644 testsuite/tests/publish/private-indexes/my_index/index/cr/crate/crate-1.0.0.toml create mode 100644 testsuite/tests/publish/private-indexes/my_index/index/index.toml create mode 100644 testsuite/tests/publish/private-indexes/test.py create mode 100644 testsuite/tests/publish/private-indexes/test.yaml create mode 100644 testsuite/tests/publish/ssh-remote-origin/test.py create mode 100644 testsuite/tests/publish/ssh-remote-origin/test.yaml diff --git a/doc/catalog-format-spec.md b/doc/catalog-format-spec.md index 5dabab539..2df346ffa 100644 --- a/doc/catalog-format-spec.md +++ b/doc/catalog-format-spec.md @@ -182,14 +182,18 @@ static, i.e. they cannot depend on the context. "Bob For Instance "] ``` - - `maintainers-logins`: mandatory (for indexing) array of strings. Flat - list of github login usernames used by the maintainers of the crate. This - information is used to authorize crate modifications. For instance: + - `maintainers-logins`: optional array of non-empty strings. + For crates submitted to the community index, this is a mandatory flat list of + the GitHub login usernames authorized to modify the crate. + For instance: ```toml maintainers-logins = ["alicehacks", "bobcoder"] ``` + Private indexes may use whichever logins are appropriate for their + hosting arrangement, or none at all. + - `licenses`: mandatory (for indexing) string. A valid [SPDX expression](https://spdx.org/licenses/). Custom license identifiers are accepted with the format: `custom-[0-9a-zA-Z.-]+` diff --git a/doc/publishing.md b/doc/publishing.md index 8ec90c3c9..775412af4 100644 --- a/doc/publishing.md +++ b/doc/publishing.md @@ -313,15 +313,32 @@ This will be shown as: ## Publishing to a local/private index -Having a local index may be useful sometimes, be it for local testing, or for -private crates not intended for publication. - -There is no practical difference between the community index that is cloned -locally and a private local index stored on disk. Hence, after obtaining the -manifest file with `alr publish`, it is a matter of placing it at the expected -location within the index: `/path/to/index/cr/crate_name/crate_name-x.x.x.toml` - -If the crate being published locally contains `"provides"` definitions, it is -necessary to call `alr index --update-all` once to ensure it is properly used -by the dependency solver. This is only necessary for the first release in a -crate that uses the `"provides"` feature. +Having a local or private index may be useful sometimes, be it for local +testing, or for private crates not intended for publication. + +There is no practical difference between the community index and a private index +stored locally on disk or on your own infrastructure. An index must be an +accessible path or URL at which an `index.toml` file can be found, either at the +root or in any immediate subdirectory. This file specifies the version of the +index, containing one line with the form `version = "0.0.0"`. No files should be placed in the same +location as `index.toml` except for the manifests of published crates. + +To start using such an index, run + +`alr index --add= --name=`, + +where `` is a human-friendly label that `alr` will use to refer to it. + +To publish a crate to a private index, run + +`alr publish --for-private-index [ ]` + +as described in the sections above, then place the manifest file it generates at +the indicated path (relative to the location of `index.toml`). + +Additions to indexes stored locally on the disk will take effect immediately, +unless the crate being published contains `"provides"` definitions, in which +case an index update will be required (either with `alr index --update-all`, or +through a scheduled auto-update) to ensure it is properly used by the dependency +solver. An index update will always be required when publishing to a remote +index. diff --git a/src/alire/alire-properties-from_toml.ads b/src/alire/alire-properties-from_toml.ads index 00bed23b6..517335b6c 100644 --- a/src/alire/alire-properties-from_toml.ads +++ b/src/alire/alire-properties-from_toml.ads @@ -53,14 +53,12 @@ package Alire.Properties.From_TOML is Crates.External_Shared_Section => (Description | Maintainers | - Maintainers_Logins | Name => True, others => False), Crates.Index_Release => (Description | Maintainers | - Maintainers_Logins | Name | Version => True, others => False), diff --git a/src/alire/alire-properties-labeled.adb b/src/alire/alire-properties-labeled.adb index 4de28a105..d61d619c2 100644 --- a/src/alire/alire-properties-labeled.adb +++ b/src/alire/alire-properties-labeled.adb @@ -131,10 +131,12 @@ package body Alire.Properties.Labeled is end if; when Maintainers_Logins => - if not Utils.Is_Valid_GitHub_Username (L.Value) then + -- The crate may be published through a private index, so we don't + -- know the requirements for a valid username; reject only an + -- empty string. + if L.Value'Length = 0 then From.Checked_Error - ("maintainers-logins must be a valid GitHub login, but got: " - & L.Value); + ("maintainers-logins values must be non-empty"); end if; when Tag => diff --git a/src/alire/alire-publish-submit.adb b/src/alire/alire-publish-submit.adb index 122a5ec0e..dcfc96885 100644 --- a/src/alire/alire-publish-submit.adb +++ b/src/alire/alire-publish-submit.adb @@ -298,7 +298,7 @@ package body Alire.Publish.Submit is Target : constant Absolute_Path := Local_Repo_Path / VFS.To_Native - (TOML_Index.Manifest_Path (Context.Root.Value.Name)) + (TOML_Index.Community_Manifest_Path (Context.Root.Value.Name)) / Filename; begin Directories.Create_Tree (Directories.Parent (Target)); diff --git a/src/alire/alire-publish.adb b/src/alire/alire-publish.adb index 907b751fa..e2205b152 100644 --- a/src/alire/alire-publish.adb +++ b/src/alire/alire-publish.adb @@ -173,13 +173,15 @@ package body Alire.Publish is -- New_Options -- ----------------- - function New_Options (Skip_Build : Boolean := False; - Skip_Submit : Boolean := False; - Manifest : String := Roots.Crate_File_Name) + function New_Options (Skip_Build : Boolean := False; + Skip_Submit : Boolean := False; + For_Private_Index : Boolean := False; + Manifest : String := Roots.Crate_File_Name) return All_Options - is (Manifest_File => +Manifest, - Skip_Build => Skip_Build, - Skip_Submit => Skip_Submit); + is (Manifest_File => +Manifest, + Skip_Build => Skip_Build, + Skip_Submit => Skip_Submit, + For_Private_Index => For_Private_Index); --------------- -- Git_Error -- @@ -228,8 +230,9 @@ package body Alire.Publish is ------------------- -- Check_Release -- ------------------- - -- Checks the presence of recommended/mandatory fileds in the release - procedure Check_Release (Release : Releases.Release) is + -- Checks the presence of recommended/mandatory fields in the release + procedure Check_Release (Release : Releases.Release; Context : in out Data) + is use CLIC.User_Input; Recommend : AAA.Strings.Vector; -- Optional @@ -294,6 +297,20 @@ package body Alire.Publish is end if; end loop; + -- The maintainers-logins field is mandatory only if publishing to the + -- community index + + if not Context.Options.For_Private_Index then + declare + Key_String : constant String := Tomify + (Properties.From_TOML.Maintainers_Logins'Image); + begin + if not Release.Has_Property (Key_String) then + Missing.Append (Key_String); + end if; + end; + end if; + Caret_Pre_1 := Release.Check_Caret_Warning; if not Missing.Is_Empty then @@ -312,6 +329,26 @@ package body Alire.Publish is & " not be pre-release versions."); end if; + -- If we are submitting to the community index, the maintainers-logins + -- values must be valid GitHub usernames + if not Context.Options.For_Private_Index then + for Property of Release.Maint_Logins loop + declare + Maint_Login : constant String := Property.To_TOML.As_String; + begin + if not Utils.Is_Valid_GitHub_Username (Maint_Login) then + Raise_Checked_Error ("The maintainer login '" + & Maint_Login + & "' is not a valid GitHub username"); + end if; + + -- We could also check GitHub.User_Exists at this point, but it + -- isn't worth the GitHub API call (running the testsuite a + -- couple of times would trigger GitHub's rate limits) + end; + end loop; + end if; + -- Final confirmation. We default to Yes if no recommended missing or -- Force. @@ -392,7 +429,8 @@ package body Alire.Publish is (Starting_Manifest (Context), Alire.Manifest.Local, Strict => True, - Root_Path => Adirs.Full_Name (+Context.Path))); + Root_Path => Adirs.Full_Name (+Context.Path)), + Context); -- Will have raised if the release is not loadable or incomplete else declare @@ -408,7 +446,7 @@ package body Alire.Publish is ("Invalid metadata found at " & Root.Value.Path, Root.Brokenness)); when Valid => - Check_Release (Root.Value.Release); + Check_Release (Root.Value.Release, Context); end case; end; end if; @@ -557,8 +595,8 @@ package body Alire.Publish is ("Your index manifest file has been generated at " & TTY.URL (Index_Manifest)); - -- Ask to submit, or show the upload URL if submission skipped, or a - -- more generic message otherwise (when lacking a github login). + -- Ask to submit, or provide submission instructions if submission + -- skipped. if not Context.Options.Skip_Submit then -- Safeguard to avoid tests creating a live pull request, unless @@ -580,23 +618,40 @@ package body Alire.Publish is then raise Early_Stop; end if; + elsif Context.Options.For_Private_Index then + -- We are publishing to a private index, the location of which is + -- unknown, so we can only give generic instructions on where to + -- place the file. + Put_Info + ("Please upload this to the index in the " + & TTY.URL (String (TOML_Index.Manifest_Path (Name)) & "/") + & " subdirectory."); elsif not Settings.Builtins.User_Github_Login.Is_Empty then + -- The user has provided a GitHub login, so provide an upload URL + -- to create a pull request. Put_Info - ("Please upload this file to " + ("If you haven't already, please fork " + & TTY.URL (Tail (Index.Community_Repo, '+')) + & " to your GitHub."); + Put_Info + ("This file can then be uploaded to " & TTY.URL (Index.Community_Host & "/" & Settings.Builtins.User_Github_Login.Get & "/" & Index.Community_Repo_Name & "/upload/" & Index.Community_Branch & "/" - & String (TOML_Index.Manifest_Path (Name))) + & String (TOML_Index.Community_Manifest_Path (Name))) & " to create a pull request against the community index."); else + -- We don't have the user's GitHub username, so show a more + -- generic message. Put_Info ("Please create a pull request against the community index at " & TTY.URL (Tail (Index.Community_Repo, '+')) & " including this file at " - & TTY.URL (String (TOML_Index.Manifest_Path (Name)))); + & TTY.URL + (String (TOML_Index.Community_Manifest_Path (Name)) & "/")); end if; exception @@ -797,7 +852,7 @@ package body Alire.Publish is Root_Path => Adirs.Full_Name (+Context.Path)) .Replacing (Origin => Context.Origin); begin - Check_Release (Release); + Check_Release (Release, Context); end Show_And_Confirm; ------------------- @@ -867,18 +922,20 @@ package body Alire.Publish is if URI.Scheme (URL) not in URI.HTTP then -- A git@ URL is private to the user and should not be used for - -- packaging: + -- packaging via the the community index if AAA.Strings.Has_Prefix (URL, "git@") then - Raise_Checked_Error - ("The origin cannot use a private git remote: " & URL); - end if; + 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 - - 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. + 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. + end if; end if; Put_Success ("Origin is of supported kind: " & Context.Origin.Kind'Img); @@ -1017,9 +1074,13 @@ package body Alire.Publish is Run_Steps (Context, (Step_Check_User_Manifest, Step_Prepare_Archive, - Step_Verify_Origin, - Step_Verify_Github, - Step_Deploy_Sources, + Step_Verify_Origin) + & + (if Options.Skip_Submit + then No_Steps + else (1 => Step_Verify_Github)) + & + (Step_Deploy_Sources, Step_Check_Build, Step_Show_And_Confirm, Step_Generate_Index_Manifest) @@ -1164,9 +1225,16 @@ package body Alire.Publish is -- requires the owner keys. case URI.Scheme (Fetch_URL) is when URI.VCS_Schemes => - Raise_Checked_Error - ("The remote URL seems to require repository ownership: " - & Fetch_URL); + 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, @@ -1249,9 +1317,13 @@ package body Alire.Publish is Token => <>); begin Run_Steps (Context, - (Step_Verify_Origin, - Step_Verify_Github, - Step_Deploy_Sources, + (Step_Verify_Origin) + & + (if Options.Skip_Submit + then No_Steps + else (1 => Step_Verify_Github)) + & + (Step_Deploy_Sources, Step_Check_Build, Step_Show_And_Confirm, Step_Generate_Index_Manifest) diff --git a/src/alire/alire-publish.ads b/src/alire/alire-publish.ads index 1b256fe9b..9c892a4c9 100644 --- a/src/alire/alire-publish.ads +++ b/src/alire/alire-publish.ads @@ -8,9 +8,10 @@ package Alire.Publish is type All_Options is private; - function New_Options (Skip_Build : Boolean := False; - Skip_Submit : Boolean := False; - Manifest : String := Roots.Crate_File_Name) + function New_Options (Skip_Build : Boolean := False; + Skip_Submit : Boolean := False; + For_Private_Index : Boolean := False; + Manifest : String := Roots.Crate_File_Name) return All_Options; procedure Directory_Tar (Path : Any_Path := "."; @@ -55,9 +56,10 @@ package Alire.Publish is private type All_Options is tagged record - Manifest_File : UString; - Skip_Build : Boolean := False; - Skip_Submit : Boolean := False; + Manifest_File : UString; + Skip_Build : Boolean := False; + Skip_Submit : Boolean := False; + For_Private_Index : Boolean := False; end record; function Manifest (Options : All_Options) return Any_Path diff --git a/src/alire/alire-releases.ads b/src/alire/alire-releases.ads index f0ed1a32a..8e8ec152b 100644 --- a/src/alire/alire-releases.ads +++ b/src/alire/alire-releases.ads @@ -287,6 +287,8 @@ package Alire.Releases is function Maintainer (R : Release) return Alire.Properties.Vector; + function Maint_Logins (R : Release) return Alire.Properties.Vector; + function Milestone (R : Release) return Milestones.Milestone; function Website (R : Release) return Alire.Properties.Vector with @@ -521,6 +523,10 @@ private is (Conditional.Enumerate (R.Properties).Filter (Alire.TOML_Keys.Maintainer)); + function Maint_Logins (R : Release) return Alire.Properties.Vector + is (Conditional.Enumerate (R.Properties).Filter + (Alire.TOML_Keys.Maint_Logins)); + function Website (R : Release) return Alire.Properties.Vector is (Conditional.Enumerate (R.Properties).Filter (Alire.TOML_Keys.Website)); diff --git a/src/alire/alire-toml_index.adb b/src/alire/alire-toml_index.adb index 8534d77e1..e22a7096d 100644 --- a/src/alire/alire-toml_index.adb +++ b/src/alire/alire-toml_index.adb @@ -584,7 +584,14 @@ package body Alire.TOML_Index is Name : constant String := +Crate; begin return Portable_Path - ("index/" & Name (Name'First .. Name'First + 1) & "/" & Name); + (Name (Name'First .. Name'First + 1) & "/" & Name); end Manifest_Path; + ----------------------------- + -- Community_Manifest_Path -- + ----------------------------- + + function Community_Manifest_Path (Crate : Crate_Name) return Portable_Path + is ("index/" & Manifest_Path (Crate)); + end Alire.TOML_Index; diff --git a/src/alire/alire-toml_index.ads b/src/alire/alire-toml_index.ads index 7a481ca5a..4de76865d 100644 --- a/src/alire/alire-toml_index.ads +++ b/src/alire/alire-toml_index.ads @@ -18,6 +18,11 @@ package Alire.TOML_Index is -- Get the expected location of a crate manifest in an index. The result is -- portable; that is, always uses forward slashes. + function Community_Manifest_Path (Crate : Crate_Name) return Portable_Path; + -- Get the expected location of a crate manifest according to the community + -- index's convention (i.e. with everything under the "index/" directory). + -- The result is portable; that is, always uses forward slashes. + procedure Load (Index : Index_On_Disk.Index'Class; Strict : Boolean; diff --git a/src/alire/alire-utils-user_input-query_config.adb b/src/alire/alire-utils-user_input-query_config.adb index 50e110c3c..2f0928710 100644 --- a/src/alire/alire-utils-user_input-query_config.adb +++ b/src/alire/alire-utils-user_input-query_config.adb @@ -41,15 +41,23 @@ package body Alire.Utils.User_Input.Query_Config is Default => "Your Name", Validation => null)); + --------------------------------------- + -- Is_Empty_Or_Valid_GitHub_Username -- + --------------------------------------- + + function Is_Empty_Or_Valid_GitHub_Username (Str : String) return Boolean + is (Str = "" or else Is_Valid_GitHub_Username (Str)); + ----------------------- -- User_GitHub_Login -- ----------------------- function User_GitHub_Login return String - is (Config_Or_Query_String (Config_Key => "user.github_login", - Question => "Please enter your GitHub login:", - Default => "github-username", - Validation => Is_Valid_GitHub_Username'Access)); + is (Config_Or_Query_String + (Config_Key => "user.github_login", + Question => "Please enter your GitHub login:", + Default => "", + Validation => Is_Empty_Or_Valid_GitHub_Username'Access)); ----------------- -- Check_Email -- diff --git a/src/alire/alire-utils-user_input-query_config.ads b/src/alire/alire-utils-user_input-query_config.ads index 79aede3ab..5ce459b5c 100644 --- a/src/alire/alire-utils-user_input-query_config.ads +++ b/src/alire/alire-utils-user_input-query_config.ads @@ -26,7 +26,9 @@ package Alire.Utils.User_Input.Query_Config is function User_Name return String; function User_GitHub_Login return String - with Post => (Is_Valid_GitHub_Username (User_GitHub_Login'Result)); + with Post => + (User_GitHub_Login'Result = "" + or else Is_Valid_GitHub_Username (User_GitHub_Login'Result)); function User_Email return String with Post => Could_Be_An_Email (User_Email'Result, With_Name => False); diff --git a/src/alr/alr-commands-init.adb b/src/alr/alr-commands-init.adb index 0895c203f..17f378eba 100644 --- a/src/alr/alr-commands-init.adb +++ b/src/alr/alr-commands-init.adb @@ -269,7 +269,9 @@ package body Alr.Commands.Init is Put_Line ("authors = " & Arr (Q (Username))); Put_Line ("maintainers = " & Arr (Q (Username & " <" & Email & ">"))); - Put_Line ("maintainers-logins = " & Arr (Q (Login))); + if Login /= "" then + Put_Line ("maintainers-logins = " & Arr (Q (Login))); + end if; Put_Line ("licenses = " & Q (Info.Licenses)); Put_Line ("website = " & Q (Info.Website)); Put_Line ("tags = " & Q_Arr (Info.Tags)); @@ -473,6 +475,23 @@ package body Alr.Commands.Init is end if; end Query_License; + ------------------------ + -- Query_GitHub_Login -- + ------------------------ + + procedure Query_GitHub_Login (Info : in out Crate_Init_Info) is + begin + if Alire.Settings.Builtins.User_Github_Login.Is_Empty then + AAA.Text_IO.Put_Paragraph + ("If you intend to publish this crate to the community index, you " + & "will need a GitHub account with which to submit a pull " + & "request, which can optionally be configured now (leave blank " + & "to skip)."); + end if; + Info.GitHub_Login := To_Unbounded_String + (UI.Query_Config.User_GitHub_Login); + end Query_GitHub_Login; + ---------------------- -- Query_Crate_Kind -- ---------------------- @@ -582,27 +601,16 @@ package body Alr.Commands.Init is is use Alire.Settings; Info : Crate_Init_Info; + User_Not_Already_Configured : constant Boolean := + Builtins.User_Email.Is_Empty + or else Builtins.User_Name.Is_Empty + or else Builtins.User_Github_Login.Is_Empty; begin if Cmd.Bin and then Cmd.Lib then Reportaise_Wrong_Arguments ("Please provide either --bin or --lib"); end if; - if Builtins.User_Email.Is_Empty or else - Builtins.User_Name.Is_Empty or else - Builtins.User_Github_Login.Is_Empty - then - AAA.Text_IO.Put_Paragraph - ("Alire needs some user information to initialize the crate" - & " author and maintainer, for eventual submission to" - & " the Alire community index. This information will be" - & " interactively requested now."); - TIO.New_Line; - TIO.Put_Line - ("You can edit this information at any time with 'alr config'"); - TIO.New_Line; - end if; - Query_Crate_Name (Args, Info); if Cmd.Bin then @@ -616,11 +624,30 @@ package body Alr.Commands.Init is Query_Description (Info); -- Query User info + if User_Not_Already_Configured then + TIO.New_Line; + AAA.Text_IO.Put_Paragraph + ("Alire needs some user information to prepare the crate for " + & "eventual submission to an index, which will be interactively " + & "requested now."); + TIO.New_Line; + TIO.Put_Line + ("You can edit this information at any time with 'alr config'"); + TIO.New_Line; + end if; Info.Username := To_Unbounded_String (UI.Query_Config.User_Name); - Info.GitHub_Login := To_Unbounded_String - (UI.Query_Config.User_GitHub_Login); + Query_GitHub_Login (Info); Info.Email := To_Unbounded_String (UI.Query_Config.User_Email); + -- Make it clear that the remainder can't be changed with `alr config` + TIO.New_Line; + if User_Not_Already_Configured then + AAA.Text_IO.Put_Paragraph + ("Alire needs some further crate-specific information to help " + & "other people who want to use your crate."); + end if; + TIO.New_Line; + Query_License (Info); Query_Tags (Info); diff --git a/src/alr/alr-commands-publish.adb b/src/alr/alr-commands-publish.adb index 018669eb5..f94a7369f 100644 --- a/src/alr/alr-commands-publish.adb +++ b/src/alr/alr-commands-publish.adb @@ -29,12 +29,15 @@ package body Alr.Commands.Publish is Options : constant Alire.Publish.All_Options := Alire.Publish.New_Options - (Manifest => + (Manifest => (if Cmd.Manifest.all /= "" then Cmd.Manifest.all else Alire.Roots.Crate_File_Name), - Skip_Build => Cmd.Skip_Build, - Skip_Submit => Cmd.Skip_Submit); + Skip_Build => Cmd.Skip_Build, + Skip_Submit => + -- "--for-private-index" implies "--skip-submit" + Cmd.Skip_Submit or else Cmd.For_Private_Index, + For_Private_Index => Cmd.For_Private_Index); begin if Alire.Utils.Count_True @@ -166,6 +169,13 @@ package body Alr.Commands.Publish is "", "--skip-submit", "Do not create the online pull request onto the community index"); + Define_Switch + (Config, + Cmd.For_Private_Index'Access, + "", "--for-private-index", + "The same as --skip-submit, but additionally disable checks which " + & "are specific to the community index and may not apply to others"); + Define_Switch (Config, Cmd.Cancel'Access, diff --git a/src/alr/alr-commands-publish.ads b/src/alr/alr-commands-publish.ads index 3ca816a4b..9b743c072 100644 --- a/src/alr/alr-commands-publish.ads +++ b/src/alr/alr-commands-publish.ads @@ -55,7 +55,7 @@ package Alr.Commands.Publish is overriding function Usage_Custom_Parameters (Cmd : Command) return String - is ("[--skip-build] [--skip-submit] [--tar] " + is ("[--skip-build] [--skip-submit|--for-private-index] [--tar] " & "[--manifest ] [ [commit]]] [--request-review NUM]"); private @@ -70,7 +70,12 @@ private -- Skip the build check Skip_Submit : aliased Boolean := False; - -- Stop after generation instead of asking the user to continue + -- Skip checking user's GitHub account, and stop after manifest + -- generation instead of asking the user to continue + + For_Private_Index : aliased Boolean := False; + -- Skip_Submit, and also disable checks which only apply to the + -- community index Cancel : aliased GNAT.Strings.String_Access := new String'(Unset); -- Number of a PR to prematurely close diff --git a/testsuite/drivers/alr.py b/testsuite/drivers/alr.py index 18f52109b..c2ac9ae63 100644 --- a/testsuite/drivers/alr.py +++ b/testsuite/drivers/alr.py @@ -123,17 +123,7 @@ def run_alr(*args, **kwargs): argv.insert(1, '-q') argv.extend(args) p = Run(argv) - if (p.status != 0 and complain_on_error) or (p.status == 0 and not complain_on_error): - print('The following command:') - print(' {}'.format(' '.join(quote_arg(arg) for arg in argv))) - print('Exited with status code {}'.format(p.status)) - print('Output:') - print(p.out) - if complain_on_error: - raise CalledProcessError('alr returned non-zero status code') - else: - raise CalledProcessError('alr returned zero status code but ' - 'an error was expected') + _report_unexpected_exit_status(p.status, complain_on_error, argv, p.out) # Convert CRLF line endings (Windows-style) to LF (Unix-style). This # canonicalization is necessary to make output comparison work on all @@ -141,7 +131,8 @@ def run_alr(*args, **kwargs): return ProcessResult(p.status, p.out.replace('\r\n', '\n')) -def run_alr_interactive(args: [str], output: [str], input: [str], timeout=5) -> str: +def run_alr_interactive(args: list[str], output: list[str], input: list[str], + timeout=5, complain_on_error=True) -> str: """ NON-WINDOWS-ONLY Run "alr" with the given arguments, feeding it the given input. No other @@ -151,7 +142,13 @@ def run_alr_interactive(args: [str], output: [str], input: [str], timeout=5) -> :param output: List of strings expected to be output by the subprocess. :param input: List of strings to feed to the subprocess's standard input. :param timeout: Timeout in seconds for the subprocess to complete. + :param complain_on_error: If True and the subprocess exits with a non-zero + status code, print information on the standard output (for debugging) + and raise a CalledProcessError (to abort the test). + Conversely if False and the process ends without error, it's presumed + an error was expected and CalledProcessError is raised too. """ + # Check whether on Windows to fail early (revisit if pexpect is updated?) if platform.system() == "Windows": print('SKIP: pexpect unavailable on Windows') @@ -177,12 +174,37 @@ def run_alr_interactive(args: [str], output: [str], input: [str], timeout=5) -> f"{child.before.decode('utf-8')}") # Assert proper output code - assert child.exitstatus == 0, \ - f"Unexpected exit status: {child.exitstatus}\n" + \ - f"Output: {child.before.decode('utf-8')}" + output = child.before.decode('utf-8') + _report_unexpected_exit_status( + child.exitstatus, complain_on_error, ["alr"] + args, output + ) # Return command output with CRLF replaced by LF (as does run_alr) - return child.before.decode('utf-8').replace('\r\n', '\n') + return output.replace('\r\n', '\n') + + +def _report_unexpected_exit_status(exit_status, complain_on_error, args, output): + """ + Report if a command yielded an unexpected exit status. + + If complain_on_error is True and exit_status is non-zero, or if it is False + and exit_status is zero, print the command and its output, then raise a + CalledProcessError. Otherwise, do nothing. + """ + error_occured = (exit_status != 0) + if (error_occured == complain_on_error): + command = " ".join(quote_arg(arg) for arg in args) + print('The following command:') + print(f' {command}') + print(f'Exited with status code {exit_status}') + print('Output:') + print(output) + if complain_on_error: + raise CalledProcessError('alr returned non-zero status code') + else: + raise CalledProcessError( + 'alr returned zero status code but an error was expected' + ) def fixtures_path(*args): @@ -282,7 +304,8 @@ def index_version(): return index_branch().split('-')[1] -def init_local_crate(name="xxx", binary=True, enter=True, update=True): +def init_local_crate(name="xxx", binary=True, enter=True, update=True, + with_maintainer_login=False): """ Initialize a local crate and enter its folder for further testing. @@ -291,16 +314,23 @@ def init_local_crate(name="xxx", binary=True, enter=True, update=True): :param bool binary: Initialize as --bin or --lib :param bool enter: Enter the created crate directory + + :param bool with_maintainer_login: Set the value of the `maintainers-logins` + field of the manifest to `["github-username"]` so that the crate is + valid for submission to the community index. """ run_alr("init", name, "--bin" if binary else "--lib") + os.chdir(name) if update: - os.chdir(name) run_alr("update") - os.chdir("..") - if enter: - os.chdir(name) + if with_maintainer_login: + with open("alire.toml", "a") as f: + f.write('maintainers-logins = ["github-username"]\n') + + if not enter: + os.chdir("..") def alr_workspace_cache(): diff --git a/testsuite/drivers/asserts.py b/testsuite/drivers/asserts.py index 7947044c4..9b6215c90 100644 --- a/testsuite/drivers/asserts.py +++ b/testsuite/drivers/asserts.py @@ -143,3 +143,11 @@ def assert_substring(target: str, text: str): """ assert target in text, \ f"Missing expected string '{target}' in text:\n{text}" + + +def assert_not_substring(target: str, text: str): + """ + Check that a string is not contained in another string + """ + assert target not in text, \ + f"Unexpected string '{target}' in text:\n{text}" diff --git a/testsuite/drivers/helpers.py b/testsuite/drivers/helpers.py index b61403216..986a9db66 100644 --- a/testsuite/drivers/helpers.py +++ b/testsuite/drivers/helpers.py @@ -293,3 +293,71 @@ def __exit__(self, exc_type, exc_val, exc_tb): import fcntl fcntl.flock(self.lock_file.fileno(), fcntl.LOCK_UN) self.lock_file.close() + + +GIT_WRAPPER_TEMPLATE = """\ +#! /usr/bin/env python +import subprocess, sys +substitution_dict = {substitution_dict} +# Argument substitutions +args = sys.argv[1:] +for key in substitution_dict: + args = [arg.replace(key, substitution_dict[key]) for arg in args] +# Run git +p = subprocess.run(['{actual_git_path}'] + args, capture_output=True) +# Output substitutions +stdout, stderr = p.stdout.decode(), p.stderr.decode() +for key in substitution_dict: + stdout = stdout.replace(substitution_dict[key], key) + stderr = stderr.replace(substitution_dict[key], key) +print(stdout, end="") +print(stderr, file=sys.stderr, end="") +# Exit with appropriate error code +sys.exit(p.returncode) +""" + +class MockGit: + """ + A context manager which mocks the git command with string substitutions. + + The string substitutions are specified by the dictionary substitution_dict. + Every non-overlapping occurrence of each of its keys in a command line + argument is replaced with its corresponding value before being passed to + git. The reverse substitution is applied to git's output. The substitutions + are applied in the order in which they appear in substitution_dict. + + The mocked version of git will be placed in mock_git_dir, which will be + temporarily added to PATH. + """ + + def __init__(self, substitution_dict, mock_git_dir): + self._substitution_dict = substitution_dict + self._mock_git_dir = mock_git_dir + + def __enter__(self): + # Create a wrapper script for git + wrapper_script = GIT_WRAPPER_TEMPLATE.format( + substitution_dict=self._substitution_dict, + actual_git_path=shutil.which("git") + ) + # Write the script to somewhere on PATH + try: + os.mkdir(self._mock_git_dir) + except FileExistsError: + pass + os.environ["PATH"] = f'{self._mock_git_dir}:{os.environ["PATH"]}' + wrapper_descriptor = os.open( + os.path.join(self._mock_git_dir, "git"), + flags=(os.O_WRONLY | os.O_CREAT | os.O_EXCL), + mode=0o764, + ) + with open(wrapper_descriptor, "w") as f: + f.write(wrapper_script) + + def __exit__(self, type, value, traceback): + # Restore PATH + os.environ["PATH"] = os.environ["PATH"].replace( + f'{self._mock_git_dir}:', '', 1 + ) + # Delete the wrapper script + os.remove(os.path.join(self._mock_git_dir, "git")) diff --git a/testsuite/tests/index/maint-bad-login/my_index/index/he/hello_world/hello_world-0.1.0.toml b/testsuite/tests/index/maint-bad-login/my_index/index/he/hello_world/hello_world-0.1.0.toml index df813e411..4cd12a989 100644 --- a/testsuite/tests/index/maint-bad-login/my_index/index/he/hello_world/hello_world-0.1.0.toml +++ b/testsuite/tests/index/maint-bad-login/my_index/index/he/hello_world/hello_world-0.1.0.toml @@ -3,7 +3,7 @@ name = "hello_world" version = "0.1.0" licenses = "GPL-3.0-only" maintainers = ["Mr. User "] -maintainers-logins = ["mr.user"] +maintainers-logins = [""] [origin] url = "file:." diff --git a/testsuite/tests/index/maint-bad-login/test.py b/testsuite/tests/index/maint-bad-login/test.py index 2e37dd915..38d6acba3 100644 --- a/testsuite/tests/index/maint-bad-login/test.py +++ b/testsuite/tests/index/maint-bad-login/test.py @@ -1,5 +1,5 @@ """ -Test that maintainers provide a plausible GitHub login +Test that maintainers-logins values can't be empty strings """ from drivers.alr import run_alr @@ -10,7 +10,7 @@ complain_on_error=False, debug=False, quiet=True) assert_match( '.*Loading .*hello_world-0.1.0.toml:.*maintainers-logins:.*' - 'maintainers-logins must be a valid GitHub login, but got: mr.user\n', + 'maintainers-logins values must be non-empty\n', p.out) print('SUCCESS') diff --git a/testsuite/tests/init/github-login/test.py b/testsuite/tests/init/github-login/test.py new file mode 100644 index 000000000..90a865c62 --- /dev/null +++ b/testsuite/tests/init/github-login/test.py @@ -0,0 +1,103 @@ +""" +Check optional input of user.github_login setting and maintainers-logins field +""" + +import os +import shutil + +from drivers.alr import run_alr, run_alr_interactive +from drivers.asserts import assert_eq, assert_substring, assert_not_substring + + +USERNAME_PROMPT = ( + r"If you intend to publish this crate to the community index, you will " + r"need a (\r\n|\r|\n)GitHub account with which to submit a pull request, " + r"which can optionally be (\r\n|\r|\n)configured now \(leave blank to " + r"skip\)\.(\r\n|\r|\n)Please enter your GitHub login: \(default: ''\)" +) + + +# `alr init` a crate without specifying a login. The resulting manifest should +# not contain a `maintainers-logins` field, and user.github_login should remain +# unset. +outputs, inputs = zip(*[ + ("Select the kind of crate you want to create", ""), + ("Enter a short description of the crate", ""), + ("Please enter your full name", ""), + (USERNAME_PROMPT, ""), + ("Please enter your email address", ""), + ("Select a software license for the crate", ""), + ("Enter a comma \(','\) separated list of tags", ""), + ("Enter an optional Website URL for the crate", ""), +]) +run_alr_interactive( + ['init', 'xxx'], + output=outputs, + input=inputs, + timeout=3 +) +assert_eq( + "\n", + run_alr("settings", "--global", "user.github_login").out +) +with open(os.path.join("xxx", "alire.toml")) as f: + assert_not_substring('maintainers-logins', f.read()) + +# Clean up for next test +shutil.rmtree("xxx") + +# Check inputs which aren't valid GitHub logins are rejected, then check +# configuring a valid login. The configured login should appear in the manifest +# file, and in the output of `alr settings` under `user.github_login`. +outputs, inputs = zip(*[ + ("Select the kind of crate you want to create", "" ), + ("Enter a short description of the crate", "" ), + ("Please enter your full name", "" ), + (USERNAME_PROMPT, "invalid_for_GitHub"), + (r"Invalid answer.[\r\n]+Please enter your GitHub", "valid-user-name" ), + ("Please enter your email address", "" ), + ("Select a software license for the crate", "" ), + ("Enter a comma \(','\) separated list of tags", "" ), + ("Enter an optional Website URL for the crate", "" ), +]) +run_alr_interactive( + ['init', 'xxx'], + output=outputs, + input=inputs, + timeout=3 +) +assert_eq( + "user.github_login=valid-user-name\n", + run_alr("settings", "--global", "user.github_login").out +) +with open(os.path.join("xxx", "alire.toml")) as f: + assert_substring('maintainers-logins = ["valid-user-name"]', f.read()) +shutil.rmtree("xxx") + +# Now that a username has been configured, check that the prompt is skipped +# and user.github_login is used instead. +outputs, inputs = zip(*[ + ("Select the kind of crate you want to create", ""), + ("Enter a short description of the crate", ""), + ("Please enter your full name", ""), + ("Please enter your email address", ""), + ("Select a software license for the crate", ""), + ("Enter a comma \(','\) separated list of tags", ""), + ("Enter an optional Website URL for the crate", ""), +]) +run_alr_interactive( + ['init', 'xxx'], + output=outputs, + input=inputs, + timeout=3 +) +assert_eq( + "user.github_login=valid-user-name\n", + run_alr("settings", "--global", "user.github_login").out +) +with open(os.path.join("xxx", "alire.toml")) as f: + assert_substring('maintainers-logins = ["valid-user-name"]', f.read()) +shutil.rmtree("xxx") + + +print('SUCCESS') diff --git a/testsuite/tests/init/github-login/test.yaml b/testsuite/tests/init/github-login/test.yaml new file mode 100644 index 000000000..32c747b3f --- /dev/null +++ b/testsuite/tests/init/github-login/test.yaml @@ -0,0 +1 @@ +driver: python-script diff --git a/testsuite/tests/monorepo/basic/test.py b/testsuite/tests/monorepo/basic/test.py index 4d5137170..e3c1bc0d7 100644 --- a/testsuite/tests/monorepo/basic/test.py +++ b/testsuite/tests/monorepo/basic/test.py @@ -16,7 +16,7 @@ start_dir = os.getcwd() os.mkdir("monoproject.upstream") os.chdir("monoproject.upstream") -init_local_crate("mycrate", enter=False) +init_local_crate("mycrate", enter=False, with_maintainer_login=True) os.chdir(start_dir) commit = init_git_repo("monoproject.upstream") diff --git a/testsuite/tests/monorepo/doubly-nested/test.py b/testsuite/tests/monorepo/doubly-nested/test.py index 6995e796a..5f646800e 100644 --- a/testsuite/tests/monorepo/doubly-nested/test.py +++ b/testsuite/tests/monorepo/doubly-nested/test.py @@ -15,8 +15,8 @@ index_dir = os.path.join(os.getcwd(), "my_index") os.mkdir("monoproject.upstream") os.chdir("monoproject.upstream") -init_local_crate("myparent") -init_local_crate("mychild") +init_local_crate("myparent", with_maintainer_login=True) +init_local_crate("mychild", with_maintainer_login=True) os.chdir(start_dir) commit = init_git_repo("monoproject.upstream") diff --git a/testsuite/tests/monorepo/manifest-in-place/test.py b/testsuite/tests/monorepo/manifest-in-place/test.py index b6c2e129f..59e2c7388 100644 --- a/testsuite/tests/monorepo/manifest-in-place/test.py +++ b/testsuite/tests/monorepo/manifest-in-place/test.py @@ -17,7 +17,7 @@ start_dir = os.getcwd() os.mkdir("monoproject.upstream") os.chdir("monoproject.upstream") -init_local_crate("crate1", enter=False) +init_local_crate("crate1", enter=False, with_maintainer_login=True) os.chdir(start_dir) commit1 = init_git_repo("monoproject.upstream") diff --git a/testsuite/tests/monorepo/multi-commit/test.py b/testsuite/tests/monorepo/multi-commit/test.py index 344948c85..3ad5a61c9 100644 --- a/testsuite/tests/monorepo/multi-commit/test.py +++ b/testsuite/tests/monorepo/multi-commit/test.py @@ -16,7 +16,7 @@ start_dir = os.getcwd() os.mkdir("monoproject.upstream") os.chdir("monoproject.upstream") -init_local_crate("crate1", enter=False) +init_local_crate("crate1", enter=False, with_maintainer_login=True) os.chdir(start_dir) commit1 = init_git_repo("monoproject.upstream") @@ -32,7 +32,7 @@ # We create a second crate at another commit os.chdir(os.path.join(start_dir, "monoproject.upstream")) -init_local_crate("crate2", enter=False) +init_local_crate("crate2", enter=False, with_maintainer_login=True) os.chdir(start_dir) commit2 = commit_all("monoproject.upstream") diff --git a/testsuite/tests/monorepo/subdir-in-tar/test.py b/testsuite/tests/monorepo/subdir-in-tar/test.py index 686a2bba5..460449499 100644 --- a/testsuite/tests/monorepo/subdir-in-tar/test.py +++ b/testsuite/tests/monorepo/subdir-in-tar/test.py @@ -11,7 +11,7 @@ import os # Prepare our "remote" repo -init_local_crate("xxx", enter=True) +init_local_crate("xxx", enter=True, with_maintainer_login=True) # Publish it. We need to give input to alr, so we directly call it. We use the # generated location as the "online" location, and this works because we are diff --git a/testsuite/tests/pin/branch-remote-protocols/test.py b/testsuite/tests/pin/branch-remote-protocols/test.py index 047e582c1..702bd22b3 100644 --- a/testsuite/tests/pin/branch-remote-protocols/test.py +++ b/testsuite/tests/pin/branch-remote-protocols/test.py @@ -3,18 +3,19 @@ """ import os +import shutil import subprocess from drivers.alr import alr_pin, alr_unpin, init_local_crate -from drivers.helpers import init_git_repo, git_branch +from drivers.helpers import init_git_repo, git_branch, MockGit from drivers.asserts import assert_eq # Create a crate with differing branches. init_local_crate(name="remote", enter=False) -LOCAL_REPO_PATH = os.path.join(os.getcwd(), "remote") +remote_path = os.path.join(os.getcwd(), "remote") # On the default branch, test_file contains "This is the main branch.\n". -test_file_path = os.path.join(LOCAL_REPO_PATH, "test_file") +test_file_path = os.path.join(remote_path, "test_file") with open(test_file_path, "w") as f: f.write("This is the main branch.\n") init_git_repo("remote") @@ -31,68 +32,37 @@ os.chdir("..") -# Prepare a directory on PATH at which to mock git. -ACTUAL_GIT_PATH = ( - subprocess.run(["bash", "-c", "type -p git"], capture_output=True) - .stdout.decode() - .strip() -) -MOCK_PATH = os.path.join(os.getcwd(), "mock_path") -os.mkdir(MOCK_PATH) -os.environ["PATH"] = f'{MOCK_PATH}:{os.environ["PATH"]}' - - # Perform the actual tests -URLs = [ +urls = [ "git+ssh://ssh.gitlab.company-name.com/path/to/repo.git", "xyz+https://github.com/path/to/repo.git", ] -SANITISED_URLS = [ +sanitised_urls = [ "ssh://ssh.gitlab.company-name.com/path/to/repo.git", "https://github.com/path/to/repo.git", ] -CACHE_TEST_FILE_PATH = "alire/cache/pins/remote/test_file" -for URL, S_URL in zip(URLs, SANITISED_URLS): +cache_test_file_path = "alire/cache/pins/remote/test_file" +mocked_git_dir = os.path.join(os.getcwd(), "mock_path") +for url, s_url in zip(urls, sanitised_urls): # Mock git with a wrapper that naively converts the url into the local path # to the "remote" crate. - wrapper_script = "\n".join( - [ - "#! /usr/bin/env python", - "import subprocess, sys", - 'if sys.argv[1:] == ["config", "--list"]:', - f' print("remote.origin.url={S_URL}\\n")', - "else:", - " args = [", - f' ("{LOCAL_REPO_PATH}" if a == "{S_URL}" else a)', - " for a in sys.argv[1:]", - " ]", - f' subprocess.run(["{ACTUAL_GIT_PATH}"] + args).check_returncode()', - ] - ) - wrapper_descriptor = os.open( - os.path.join(MOCK_PATH, "git"), - flags=(os.O_WRONLY | os.O_CREAT | os.O_TRUNC), - mode=0o764, - ) - with open(wrapper_descriptor, "w") as f: - f.write(wrapper_script) - - # Create an empty crate, and pin the default branch of the test repo - init_local_crate() - alr_pin("remote", url=URL, branch=default_branch) - with open(CACHE_TEST_FILE_PATH) as f: - assert_eq("This is the main branch.\n", f.read()) - - # Edit pin to point to the other branch, and verify the cached copy changes - # as it should - alr_unpin("remote", update=False) - alr_pin("remote", url=URL, branch="other") - with open(CACHE_TEST_FILE_PATH) as f: - assert_eq("This is the other branch.\n", f.read()) + with MockGit({s_url: remote_path}, mocked_git_dir): + # Create an empty crate, and pin the default branch of the test repo + init_local_crate() + alr_pin("remote", url=url, branch=default_branch) + with open(cache_test_file_path) as f: + assert_eq("This is the main branch.\n", f.read()) + # Edit pin to point to the other branch, and verify the cached copy changes + # as it should + alr_unpin("remote", update=False) + alr_pin("remote", url=url, branch="other") + with open(cache_test_file_path) as f: + assert_eq("This is the other branch.\n", f.read()) -# Restore PATH -os.environ["PATH"] = os.environ["PATH"][len(MOCK_PATH) + 1 :] + # Clean up for next test + os.chdir("..") + shutil.rmtree("xxx") print("SUCCESS") diff --git a/testsuite/tests/publish/check-pre-release-version/test.py b/testsuite/tests/publish/check-pre-release-version/test.py index 58f223f4e..1b257f626 100644 --- a/testsuite/tests/publish/check-pre-release-version/test.py +++ b/testsuite/tests/publish/check-pre-release-version/test.py @@ -6,7 +6,7 @@ from drivers.asserts import assert_match from drivers.helpers import init_git_repo -init_local_crate("my_crate") +init_local_crate("my_crate", with_maintainer_login=True) p = run_alr("publish", "--tar", complain_on_error=False, quiet=False) diff --git a/testsuite/tests/publish/local-repo-branched/test.py b/testsuite/tests/publish/local-repo-branched/test.py index b2d09efcc..340c85dd9 100644 --- a/testsuite/tests/publish/local-repo-branched/test.py +++ b/testsuite/tests/publish/local-repo-branched/test.py @@ -3,6 +3,7 @@ """ from drivers.alr import init_local_crate, run_alr +from drivers.asserts import assert_match from drivers.helpers import init_git_repo from shutil import copyfile from subprocess import run @@ -10,7 +11,7 @@ import os # Prepare our "remote" repo -init_local_crate("xxx", enter=False) +init_local_crate("xxx", enter=False, with_maintainer_login=True) head_commit = init_git_repo("xxx") # Clone to a "local" repo and set minimal config @@ -27,6 +28,12 @@ assert run(["git", "push", "-u", "origin", "devel"]).returncode == 0 # Check that the publishing assistant completes without complaining -run_alr("--force", "publish", "--skip-submit") +p = run_alr("--force", "publish", "--skip-submit", quiet=False) + +# Check the user is warned that the origin URL is a local path +assert_match( + r".*The origin must be a definitive remote location, but is .*", + p.out +) print('SUCCESS') diff --git a/testsuite/tests/publish/local-repo-nonstd/test.py b/testsuite/tests/publish/local-repo-nonstd/test.py index 355cd7fd4..157ccd19e 100644 --- a/testsuite/tests/publish/local-repo-nonstd/test.py +++ b/testsuite/tests/publish/local-repo-nonstd/test.py @@ -20,7 +20,7 @@ def verify_manifest(): # Prepare our "remote" repo, changing the manifest name to "xxx.toml" -init_local_crate("xxx") +init_local_crate("xxx", with_maintainer_login=True) os.rename("alire.toml", "xxx.toml") os.chdir("..") head_commit = init_git_repo("xxx") diff --git a/testsuite/tests/publish/local-repo/test.py b/testsuite/tests/publish/local-repo/test.py index 41d27e43b..6ff3b40cd 100644 --- a/testsuite/tests/publish/local-repo/test.py +++ b/testsuite/tests/publish/local-repo/test.py @@ -20,7 +20,7 @@ def verify_manifest(): # Prepare our "remote" repo -init_local_crate("xxx", enter=False) +init_local_crate("xxx", enter=False, with_maintainer_login=True) head_commit = init_git_repo("xxx") # Clone to a "local" repo and set minimal config diff --git a/testsuite/tests/publish/pin-removal/test.py b/testsuite/tests/publish/pin-removal/test.py index 9956fdad1..f7ee1df27 100644 --- a/testsuite/tests/publish/pin-removal/test.py +++ b/testsuite/tests/publish/pin-removal/test.py @@ -16,7 +16,7 @@ # We create a repository with the nested crate that will act as the upstream # remote repository: start_dir = os.getcwd() -init_local_crate(crate) +init_local_crate(crate, with_maintainer_login=True) # And add the pin directly in the remote alr_pin("unobtanium", path="/") diff --git a/testsuite/tests/publish/private-indexes/my_index/crates/crate/alire.toml b/testsuite/tests/publish/private-indexes/my_index/crates/crate/alire.toml new file mode 100644 index 000000000..f52a77781 --- /dev/null +++ b/testsuite/tests/publish/private-indexes/my_index/crates/crate/alire.toml @@ -0,0 +1,9 @@ +name = "crate" +description = "Dummy crate" +version = "0.0.0" + +authors = ["Your Name"] +maintainers = ["Your Name "] +maintainers-logins = ["github-username"] +website = "" +tags = [] diff --git a/testsuite/tests/publish/private-indexes/my_index/crates/crate/crate.gpr b/testsuite/tests/publish/private-indexes/my_index/crates/crate/crate.gpr new file mode 100644 index 000000000..29bbd90b8 --- /dev/null +++ b/testsuite/tests/publish/private-indexes/my_index/crates/crate/crate.gpr @@ -0,0 +1,22 @@ +with "config/crate_config.gpr"; +project Crate is + + for Source_Dirs use ("src/", "config/"); + for Object_Dir use "obj/" & Crate_Config.Build_Profile; + for Create_Missing_Dirs use "True"; + for Exec_Dir use "bin"; + for Main use ("crate.adb"); + + package Compiler is + for Default_Switches ("Ada") use Crate_Config.Ada_Compiler_Switches; + end Compiler; + + package Binder is + for Switches ("Ada") use ("-Es"); -- Symbolic traceback + end Binder; + + package Install is + for Artifacts (".") use ("share"); + end Install; + +end Crate; diff --git a/testsuite/tests/publish/private-indexes/my_index/crates/crate/src/crate.adb b/testsuite/tests/publish/private-indexes/my_index/crates/crate/src/crate.adb new file mode 100644 index 000000000..27b9f460a --- /dev/null +++ b/testsuite/tests/publish/private-indexes/my_index/crates/crate/src/crate.adb @@ -0,0 +1,4 @@ +procedure Crate is +begin + null; +end Crate; diff --git a/testsuite/tests/publish/private-indexes/my_index/index/cr/crate/crate-1.0.0.toml b/testsuite/tests/publish/private-indexes/my_index/index/cr/crate/crate-1.0.0.toml new file mode 100644 index 000000000..623a83b91 --- /dev/null +++ b/testsuite/tests/publish/private-indexes/my_index/index/cr/crate/crate-1.0.0.toml @@ -0,0 +1,13 @@ +# NOTE: this crate is not used in the test, but we need at least one crate in +# the index or the community index will get cloned due to empty in-memory +# catalog. + +description = "Dummy crate" +name = "crate" +version = "1.0.0" +licenses = [] +maintainers = ["any@bo.dy"] +maintainers-logins = ["someone"] + +[origin] +url = "file:../../../crates/crate" diff --git a/testsuite/tests/publish/private-indexes/my_index/index/index.toml b/testsuite/tests/publish/private-indexes/my_index/index/index.toml new file mode 100644 index 000000000..bad265e4f --- /dev/null +++ b/testsuite/tests/publish/private-indexes/my_index/index/index.toml @@ -0,0 +1 @@ +version = "1.1" diff --git a/testsuite/tests/publish/private-indexes/test.py b/testsuite/tests/publish/private-indexes/test.py new file mode 100644 index 000000000..d3eacb0f1 --- /dev/null +++ b/testsuite/tests/publish/private-indexes/test.py @@ -0,0 +1,389 @@ +""" +Check "alr publish --for-private-index" supports private indexes +""" + + +import os +import shutil +import subprocess + +from drivers.alr import run_alr, run_alr_interactive +from drivers.helpers import init_git_repo, MockGit +from drivers.asserts import assert_match, assert_file_exists + + +INDEX_PATH = os.path.join(os.getcwd(), "my_index", "index") + + +def run(*args): + subprocess.run(*args).check_returncode() + +def test( + args, + url, + num_confirms, + output, + gen_manifest=None, + maint_logins=None, + github_user=None, + expect_success=True +): + """ + Perform the general test procedure. + + - Create a mock remote repo which appears to have a remote URL, and a local + clone thereof. + - `alr init` a crate in this repo + - Run `alr` with the specified arguments, responding `y` to the prompt + `Do you want to proceed with this information?` a specified number of + times + - Assert that `alr`'s final output matches zero or more regex patterns + - Optionally, assert that an index manifest was generated and matches zero + or more regex patterns + + :param list(str) args: The arguments to pass to `alr` (`--no-color` will be + added) + :param str url: The URL at which (as far as Alire is concerned) the remote + repository is located + :param int num_confirms: The number of times to respond `y` to the prompt + `Do you want to proceed with this information?` + :param list(str) output: Zero or more regex patterns which must match the + final output (i.e. that which follows the last confirmation prompt) of + `alr` + :param list(str) gen_manifest: Zero or more regex patterns which must match + the content of the generated manifest. If None, expects no manifest to + be generated. + :param str maint_logins: If not `None`, the value to set for the + `maintainers-logins` field in the crate's manifest before calling `alr` + :param str github_user: If not `None`, the value to set as + `user.github_login` before calling `alr` + :param bool expect_success: If True, the test will fail if `alr` returns a + non-zero exit code. If False, fail on a zero exit code. + """ + # Create an alire workspace to act as a "remote" + os.makedirs("remote") + os.chdir("remote") + run_alr("init", "--bin", "xxx") + os.chdir("xxx") + # Adjust the values of maintainers-logins and user.github_login if required + if github_user is not None: + run(["alr", "settings", "--set", "user.github_login", github_user]) + if maint_logins is not None: + with open("alire.toml", "a") as f: + f.write(f"maintainers-logins = {maint_logins}\n") + # Initialise as a git repo + init_git_repo(".") + remote_path = os.getcwd() + + # Mock git with a wrapper that naively converts the url into the local path + # to the "remote" crate. + mocked_git_dir = os.path.abspath(os.path.join("..", "..", "mocked_git")) + with MockGit({url: remote_path}, mocked_git_dir): + # Create a "local" clone of the "remote" + local_path = os.path.abspath(os.path.join("..", "..", "local", "xxx")) + os.makedirs(local_path) + os.chdir(local_path) + run(["git", "clone", url, local_path]) + + # Run alr + p = run_alr_interactive( + args, + output=num_confirms * [ + "Do you want to proceed with this information?" + ], + input=num_confirms * ["y"], + complain_on_error=expect_success, + timeout=3, + ) + + # Check output matches + for pattern in output: + assert_match(pattern, p) + + # Check the generated manifest file + gen_manifest_path = os.path.join( + os.getcwd(), "alire", "releases", "xxx-0.1.0-dev.toml" + ) + idx_manifest_dir = os.path.join(INDEX_PATH, "xx", "xxx") + os.chdir(os.path.join("..", "..")) + if gen_manifest is None: + assert_file_exists(gen_manifest_path, wanted=False) + else: + # Check existence + assert_file_exists(gen_manifest_path, wanted=True) + + # Check regex matches + with open(gen_manifest_path) as f: + manifest = f.read() + for pattern in gen_manifest: + assert_match(pattern, manifest) + + # Add this manifest to our local index + os.makedirs(idx_manifest_dir) + shutil.copyfile( + gen_manifest_path, + os.path.join(idx_manifest_dir, "xxx-0.1.0-dev.toml") + ) + + # Check that the crate can be retrieved and built without error + p = run_alr("get", "--build", "xxx", quiet=False) + assert_match( + r".*xxx=0\.1\.0-dev successfully retrieved and built.*", + p.out + ) + + # Clean up for next test + shutil.rmtree("local") + shutil.rmtree("remote") + shutil.rmtree(idx_manifest_dir, ignore_errors=True) # may not exist + + +# All tests should behave the same with and without "--force" +for force_arg in ([], ["--force"]): + # A crate suitable for the community index: + # + # Publication should succeed, with either "--for-private-index" or + # "--skip-submit" circumventing the requirement for the user to provide a + # 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", + maint_logins='["github-username"]', + num_confirms=2, + output=[ + r".*Success: Your index manifest file has been generated.*", + # Even though the automatic pull request has been skipped, alr + # should provide instructions for submission to the community index. + ( + r".*Please create a pull request against the community index " + r"at https://github.com/alire-project/alire-index including " + r"this file at index/xx/xxx/.*" + ), + ], + gen_manifest=[ + # "git+" should be prepended to avoid ambiguity + r'.*url = "git\+https://github\.com/some_user/repo-name\.git".*', + ], + expect_success=True + ) + test( + args=force_arg + ["publish", "--for-private-index"], + url="https://github.com/some_user/repo-name.git", + maint_logins='["github-username"]', + num_confirms=2, + output=[ + r".*Success: Your index manifest file has been generated.*", + # alr should provide instructions again, but they should be more + # generic, since we don't know where the private index is located. + r".*Please upload this to the index in the xx/xxx/ subdirectory.*", + ], + gen_manifest=[ + r'.*url = "git\+https://github\.com/some_user/repo-name\.git".*', + ], + expect_success=True + ) + + # 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", + maint_logins='["github-username"]', + github_user="github-username", + num_confirms=2, + output=[ + r".*Success: Your index manifest file has been generated.*", + # The user has configured a GitHub username, so a specific upload + # URL should be provided + ( + r".*If you haven't already, please fork " + r"https://github.com/alire-project/alire-index to your GitHub.*" + ), + ( + r".*This file can then be uploaded to " + r"https://github\.com/github-username/alire-index/upload/" + r"stable-1\.3\.0/index/xx/xxx to create a pull request against" + r" the community index.*" + ), + ], + gen_manifest=[ + r'.*url = "git\+https://github\.com/some_user/repo-name\.git".*', + ], + 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, # (fails before second confirmation) + 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" also currently fails, but due to an + # issue with URI recognition (the host part is not properly identified). + test( + args=force_arg + ["publish", "--for-private-index"], + url="git@bitbucket.org:/some_user/repo-name.git", + maint_logins='["github-username"]', + num_confirms=1, + output=[ + r".* Origin is hosted on unknown site:.*", + ], + gen_manifest=None, + expect_success=False + ) + + # A crate unsuitable for the community index because it has a + # "maintainers-logins" value which is invalid for GitHub: + # + # "alr publish" and "alr publish --skip-submit" should fail. + test( + args=force_arg + ["publish"], + url="https://github.com/some_user/repo-name.git", + maint_logins='["valid-for-GitHub", "invalid_for_GitHub"]', + num_confirms=0, # (fails before first confirmation) + output=[ + ( + r".*The maintainer login 'invalid_for_GitHub' " + r"is not a valid GitHub username.*" + ), + ], + gen_manifest=None, + expect_success=False + ) + test( + args=force_arg + ["publish", "--skip-submit"], + url="https://github.com/some_user/repo-name.git", + maint_logins='["valid-for-GitHub", "invalid_for_GitHub"]', + num_confirms=0, + output=[ + ( + r".*The maintainer login 'invalid_for_GitHub' " + r"is not a valid GitHub username.*" + ), + ], + gen_manifest=None, + expect_success=False + ) + # "alr publish --for-private-index" will succeed. + test( + args=force_arg + ["publish", "--for-private-index"], + url="https://github.com/some_user/repo-name.git", + maint_logins='["valid-for-GitHub", "invalid_for_GitHub"]', + num_confirms=2, + output=[ + r".*Success: Your index manifest file has been generated.*", + r".*Please upload this to the index in the xx/xxx/ subdirectory.*", + ], + gen_manifest=[ + r'.*url = "git\+https://github\.com/some_user/repo-name\.git".*', + ], + expect_success=True + ) + + # A crate unsuitable for the community index because it has no + # "maintainers-logins" value: + # + # "alr publish" and "alr publish --skip-submit" should fail. + test( + args=force_arg + ["publish"], + url="https://github.com/some_user/repo-name.git", + maint_logins=None, + num_confirms=0, # (fails before first confirmation) + output=[ + r".*Missing required properties: maintainers-logins.*", + ], + gen_manifest=None, + expect_success=False + ) + test( + args=force_arg + ["publish", "--skip-submit"], + url="https://github.com/some_user/repo-name.git", + maint_logins=None, + num_confirms=0, + output=[ + r".*Missing required properties: maintainers-logins.*", + ], + gen_manifest=None, + expect_success=False + ) + # "alr publish --for-private-index" will succeed. + test( + args=force_arg + ["publish", "--for-private-index"], + url="https://github.com/some_user/repo-name.git", + maint_logins=None, + num_confirms=2, + output=[ + r".*Success: Your index manifest file has been generated.*", + r".*Please upload this to the index in the xx/xxx/ subdirectory.*", + ], + gen_manifest=[ + r'.*url = "git\+https://github\.com/some_user/repo-name\.git".*', + ], + expect_success=True + ) + + # A crate unsuitable for the community index because "maintainers-logins" + # is an empty list: + # + # This should be identical to there being no "maintainers-logins" field at + # all. + test( + args=force_arg + ["publish"], + url="https://github.com/some_user/repo-name.git", + maint_logins="[]", + num_confirms=0, + output=[ + r".*Missing required properties: maintainers-logins.*", + ], + gen_manifest=None, + expect_success=False + ) + test( + args=force_arg + ["publish", "--skip-submit"], + url="https://github.com/some_user/repo-name.git", + maint_logins="[]", + num_confirms=0, + output=[ + r".*Missing required properties: maintainers-logins.*", + ], + gen_manifest=None, + expect_success=False + ) + test( + args=force_arg + ["publish", "--for-private-index"], + url="https://github.com/some_user/repo-name.git", + maint_logins="[]", + num_confirms=2, + output=[ + r".*Success: Your index manifest file has been generated.*", + r".*Please upload this to the index in the xx/xxx/ subdirectory.*", + ], + gen_manifest=[ + r'.*url = "git\+https://github\.com/some_user/repo-name\.git".*', + ], + expect_success=True + ) + + +print("SUCCESS") diff --git a/testsuite/tests/publish/private-indexes/test.yaml b/testsuite/tests/publish/private-indexes/test.yaml new file mode 100644 index 000000000..0a859639c --- /dev/null +++ b/testsuite/tests/publish/private-indexes/test.yaml @@ -0,0 +1,4 @@ +driver: python-script +indexes: + my_index: + in_fixtures: false diff --git a/testsuite/tests/publish/remote-origin-nonstd/test.py b/testsuite/tests/publish/remote-origin-nonstd/test.py index 2651efc2d..b4f6d1f0c 100644 --- a/testsuite/tests/publish/remote-origin-nonstd/test.py +++ b/testsuite/tests/publish/remote-origin-nonstd/test.py @@ -2,7 +2,7 @@ Test proper publishing of a ready remote origin with custom manifest location """ -from drivers.alr import run_alr, index_version +from drivers.alr import init_local_crate, run_alr, index_version from drivers.asserts import assert_match from drivers.helpers import contents, content_of, init_git_repo, zip_dir from shutil import copyfile, rmtree @@ -26,7 +26,7 @@ def verify_manifest(): run_alr("index", "--add", "my_index", "--name", "my_index") # Prepare a repo and a zipball to be used as "remote" targets for publishing -run_alr("init", "--bin", "xxx") +init_local_crate("xxx", enter=False, with_maintainer_login=True) # Rename the manifest location os.rename(os.path.join("xxx", "alire.toml"), os.path.join("xxx", "xxx.toml")) diff --git a/testsuite/tests/publish/remote-origin/test.py b/testsuite/tests/publish/remote-origin/test.py index f35c29510..bdd9ba65f 100644 --- a/testsuite/tests/publish/remote-origin/test.py +++ b/testsuite/tests/publish/remote-origin/test.py @@ -2,7 +2,7 @@ Tests for proper publishing of a ready remote origin """ -from drivers.alr import run_alr, index_version +from drivers.alr import init_local_crate, run_alr, index_version from drivers.asserts import assert_match from drivers.helpers import contents, content_of, init_git_repo, zip_dir from shutil import copyfile, rmtree @@ -26,7 +26,7 @@ def verify_manifest(): run_alr("index", "--add", "my_index", "--name", "my_index") # Prepare a repo and a zipball to be used as "remote" targets for publishing -run_alr("init", "--bin", "xxx") +init_local_crate("xxx", enter=False, with_maintainer_login=True) # Create the zip zip_dir("xxx", "xxx.zip") diff --git a/testsuite/tests/publish/ssh-remote-origin/test.py b/testsuite/tests/publish/ssh-remote-origin/test.py new file mode 100644 index 000000000..ecc69a62e --- /dev/null +++ b/testsuite/tests/publish/ssh-remote-origin/test.py @@ -0,0 +1,34 @@ +""" +Check "alr publish " only allows private origins with --for-private-index +""" + + +from drivers.alr import run_alr +from drivers.asserts import assert_match + + +urls = [ + "git@host.invalid:/path/to/repo.git", +] +commit = "0" * 40 + +# We expect attempts to publish from these origins to fail, because they are +# obviously private. +for url in urls: + p = run_alr("publish", url, commit, complain_on_error=False) + assert_match(r".*The origin cannot use a private remote:.*", p.out) + p = run_alr( + "publish", "--skip-submit", url, commit, complain_on_error=False + ) + assert_match(r".*The origin cannot use a private remote:.*", p.out) + +# Publishing will still fail with "--for-private-index", but it should be due to +# the untrusted host, not because the URLs appear private. +for url in urls: + p = run_alr( + "publish", "--for-private-index", url, commit,complain_on_error=False + ) + assert_match(r".*Origin is hosted on unknown site: .*", p.out) + + +print("SUCCESS") diff --git a/testsuite/tests/publish/ssh-remote-origin/test.yaml b/testsuite/tests/publish/ssh-remote-origin/test.yaml new file mode 100644 index 000000000..32c747b3f --- /dev/null +++ b/testsuite/tests/publish/ssh-remote-origin/test.yaml @@ -0,0 +1 @@ +driver: python-script diff --git a/testsuite/tests/publish/tarball-plaindir-nonstd/test.py b/testsuite/tests/publish/tarball-plaindir-nonstd/test.py index 0b54ebc52..3f48c71f8 100644 --- a/testsuite/tests/publish/tarball-plaindir-nonstd/test.py +++ b/testsuite/tests/publish/tarball-plaindir-nonstd/test.py @@ -11,7 +11,7 @@ import os # Prepare our "remote" repo -init_local_crate("xxx", enter=True) +init_local_crate("xxx", enter=True, with_maintainer_login=True) # with custom manifest location os.rename("alire.toml", "xxx.toml") diff --git a/testsuite/tests/publish/tarball-plaindir/test.py b/testsuite/tests/publish/tarball-plaindir/test.py index a3b910d33..46c956946 100644 --- a/testsuite/tests/publish/tarball-plaindir/test.py +++ b/testsuite/tests/publish/tarball-plaindir/test.py @@ -11,7 +11,7 @@ import os # Prepare our "remote" repo -init_local_crate("xxx", enter=True) +init_local_crate("xxx", enter=True, with_maintainer_login=True) canary = "canary.txt" diff --git a/testsuite/tests/publish/tarball-repo-nonstd/test.py b/testsuite/tests/publish/tarball-repo-nonstd/test.py index ad482832e..93578e115 100644 --- a/testsuite/tests/publish/tarball-repo-nonstd/test.py +++ b/testsuite/tests/publish/tarball-repo-nonstd/test.py @@ -3,6 +3,7 @@ """ from drivers.alr import init_local_crate, run_alr +from drivers.asserts import assert_match from drivers.helpers import init_git_repo from shutil import copyfile from subprocess import run @@ -10,7 +11,7 @@ import os # Prepare our "remote" repo -init_local_crate("xxx", enter=True) +init_local_crate("xxx", enter=True, with_maintainer_login=True) os.rename("alire.toml", "xxx.toml") # Initialize a repo right here @@ -25,11 +26,18 @@ # Publish it. We need to give input to alr, so we directly call it. We use the # generated location as the "online" location, and this works because we are # forcing. ".tgz" is used, as bzip2 is not supported by `git archive`. -p = run(["alr", "-q", "-f", "-n", "publish", "--skip-build", "--skip-submit", "--tar", +p = run(["alr", "-f", "-n", "publish", "--skip-build", "--skip-submit", "--tar", "--manifest", "xxx.toml"], - input=f"file:{os.getcwd()}/alire/archives/xxx-0.1.0-dev.tgz\n".encode()) + input=f"file:{os.getcwd()}/alire/archives/xxx-0.1.0-dev.tgz\n".encode(), + capture_output=True) p.check_returncode() +# Check user is warned that the origin URL is a local path +assert_match( + r".*The origin must be a definitive remote location, but is .*", + p.stderr.decode() +) + # Verify the index manifest has been generated assert os.path.isfile("./alire/releases/xxx-0.1.0-dev.toml") diff --git a/testsuite/tests/publish/tarball-repo/test.py b/testsuite/tests/publish/tarball-repo/test.py index 2dc953bce..aa69bd465 100644 --- a/testsuite/tests/publish/tarball-repo/test.py +++ b/testsuite/tests/publish/tarball-repo/test.py @@ -3,6 +3,7 @@ """ from drivers.alr import init_local_crate, run_alr +from drivers.asserts import assert_match from drivers.helpers import init_git_repo from shutil import copyfile from subprocess import run @@ -10,7 +11,7 @@ import os # Prepare our "remote" repo -init_local_crate("xxx", enter=True) +init_local_crate("xxx", enter=True, with_maintainer_login=True) # Initialize a repo right here init_git_repo(".") @@ -24,10 +25,17 @@ # Publish it. We need to give input to alr, so we directly call it. We use the # generated location as the "online" location, and this works because we are # forcing. ".tgz" is used, as bzip2 is not supported by `git archive`. -p = run(["alr", "-q", "-f", "-n", "publish", "--skip-build", "--skip-submit", "--tar"], - input=f"file:{os.getcwd()}/alire/archives/xxx-0.1.0-dev.tgz\n".encode()) +p = run(["alr", "-f", "-n", "publish", "--skip-build", "--skip-submit", "--tar"], + input=f"file:{os.getcwd()}/alire/archives/xxx-0.1.0-dev.tgz\n".encode(), + capture_output=True) p.check_returncode() +# Check user is warned that the origin URL is a local path +assert_match( + r".*The origin must be a definitive remote location, but is .*", + p.stderr.decode() +) + # Verify the index manifest has been generated assert os.path.isfile("./alire/releases/xxx-0.1.0-dev.toml") From a67762c836240dc56af7d39e48f53ec2ee491f4a Mon Sep 17 00:00:00 2001 From: Seb M'Caw Date: Wed, 28 Aug 2024 13:13:16 +0000 Subject: [PATCH 2/9] Fix tests --- testsuite/drivers/helpers.py | 16 +++++++++++++--- .../tests/pin/branch-remote-protocols/test.yaml | 3 --- testsuite/tests/publish/private-indexes/test.py | 2 +- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/testsuite/drivers/helpers.py b/testsuite/drivers/helpers.py index 986a9db66..b13877498 100644 --- a/testsuite/drivers/helpers.py +++ b/testsuite/drivers/helpers.py @@ -8,6 +8,7 @@ import re import shutil import stat +import sys from subprocess import run from zipfile import ZipFile @@ -318,6 +319,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): class MockGit: """ + NON-WINDOWS-ONLY A context manager which mocks the git command with string substitutions. The string substitutions are specified by the dictionary substitution_dict. @@ -335,17 +337,25 @@ def __init__(self, substitution_dict, mock_git_dir): self._mock_git_dir = mock_git_dir def __enter__(self): + # Mocking on Windows would require git.exe wrapper + if on_windows(): + print('SKIP: git mocking unavailable on Windows') + sys.exit(0) + # Create a wrapper script for git wrapper_script = GIT_WRAPPER_TEMPLATE.format( substitution_dict=self._substitution_dict, actual_git_path=shutil.which("git") ) - # Write the script to somewhere on PATH + # Add the directory to PATH try: os.mkdir(self._mock_git_dir) except FileExistsError: pass - os.environ["PATH"] = f'{self._mock_git_dir}:{os.environ["PATH"]}' + os.environ["PATH"] = ( + f'{self._mock_git_dir}{os.pathsep}{os.environ["PATH"]}' + ) + # Write the script to the directory wrapper_descriptor = os.open( os.path.join(self._mock_git_dir, "git"), flags=(os.O_WRONLY | os.O_CREAT | os.O_EXCL), @@ -357,7 +367,7 @@ def __enter__(self): def __exit__(self, type, value, traceback): # Restore PATH os.environ["PATH"] = os.environ["PATH"].replace( - f'{self._mock_git_dir}:', '', 1 + f'{self._mock_git_dir}{os.pathsep}', '', 1 ) # Delete the wrapper script os.remove(os.path.join(self._mock_git_dir, "git")) diff --git a/testsuite/tests/pin/branch-remote-protocols/test.yaml b/testsuite/tests/pin/branch-remote-protocols/test.yaml index ee8ead706..32c747b3f 100644 --- a/testsuite/tests/pin/branch-remote-protocols/test.yaml +++ b/testsuite/tests/pin/branch-remote-protocols/test.yaml @@ -1,4 +1 @@ driver: python-script -control: - - [SKIP, "skip_linux", "Test is Linux-only"] -indexes: {} diff --git a/testsuite/tests/publish/private-indexes/test.py b/testsuite/tests/publish/private-indexes/test.py index d3eacb0f1..adab48671 100644 --- a/testsuite/tests/publish/private-indexes/test.py +++ b/testsuite/tests/publish/private-indexes/test.py @@ -93,7 +93,7 @@ def test( ], input=num_confirms * ["y"], complain_on_error=expect_success, - timeout=3, + timeout=10, ) # Check output matches From 6270d06fe6c20ca40250be02b32bc86a3ca15aa2 Mon Sep 17 00:00:00 2001 From: Seb M'Caw Date: Thu, 29 Aug 2024 10:13:18 +0000 Subject: [PATCH 3/9] Support "git@" remotes --- src/alire/alire-origins.adb | 5 +++- src/alire/alire-publish.adb | 10 +++---- src/alire/alire-uri.adb | 26 +++++++++++++++++++ src/alire/alire-uri.ads | 8 ++++++ .../tests/publish/private-indexes/test.py | 16 +++++++----- .../tests/publish/ssh-remote-origin/test.py | 2 +- 6 files changed, 52 insertions(+), 15 deletions(-) diff --git a/src/alire/alire-origins.adb b/src/alire/alire-origins.adb index 658791d6e..15f130356 100644 --- a/src/alire/alire-origins.adb +++ b/src/alire/alire-origins.adb @@ -769,7 +769,10 @@ package body Alire.Origins is when VCS_Kinds => Table.Set (Keys.URL, - +(Prefixes (This.Kind).all + +((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: diff --git a/src/alire/alire-publish.adb b/src/alire/alire-publish.adb index e2205b152..78d006988 100644 --- a/src/alire/alire-publish.adb +++ b/src/alire/alire-publish.adb @@ -954,10 +954,10 @@ package body Alire.Publish is Is_Trusted (URL) then Put_Success ("Origin is hosted on trusted site: " - & URI.Authority_Without_Credentials (URL)); + & URI.Host (URL)); else Raise_Checked_Error ("Origin is hosted on unknown site: " - & URI.Authority_Without_Credentials (URL)); + & URI.Host (URL)); end if; end if; @@ -1095,10 +1095,8 @@ package body Alire.Publish is ---------------- function Is_Trusted (URL : Alire.URL) return Boolean - is (for some Site of Trusted_Sites => - URI.Authority_Without_Credentials (URL) = Site - or else - Has_Suffix (URI.Authority (URL), "." & Site)); + is (for some Site of Trusted_Sites => URI.Host (URL) = Site + or else Has_Suffix (URI.Host (URL), "." & Site)); ---------------------- -- Local_Repository -- diff --git a/src/alire/alire-uri.adb b/src/alire/alire-uri.adb index 1c8e47a48..9715b5858 100644 --- a/src/alire/alire-uri.adb +++ b/src/alire/alire-uri.adb @@ -49,6 +49,32 @@ package body Alire.URI is Unknown); end Scheme; + ---------- + -- 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 + -- This has the form git@X:Y, so return X + return Head (Tail (This, "@"), ":"); + 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 + return Head (Auth, "]:") & "]"; + else + return Head (Auth, ":"); + end if; + end if; + end Host; + package body Operators is --------- diff --git a/src/alire/alire-uri.ads b/src/alire/alire-uri.ads index d7db14cf7..4265896d7 100644 --- a/src/alire/alire-uri.ads +++ b/src/alire/alire-uri.ads @@ -77,6 +77,14 @@ package Alire.URI with Preelaborate is function Authority_Without_Credentials (This : URL) return String; -- Only the part after @ in an authority + function Host (This : URL) return String; + -- The host part of a remote URI + -- + -- 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. + function Local_Path (This : URL) return String with Pre => Scheme (This) in None | File or else raise Checked_Error with Errors.Set diff --git a/testsuite/tests/publish/private-indexes/test.py b/testsuite/tests/publish/private-indexes/test.py index adab48671..c5617dd5f 100644 --- a/testsuite/tests/publish/private-indexes/test.py +++ b/testsuite/tests/publish/private-indexes/test.py @@ -220,7 +220,7 @@ def test( args=force_arg + ["publish"], url="git@bitbucket.org:/some_user/repo-name.git", maint_logins='["github-username"]', - num_confirms=1, # (fails before second confirmation) + num_confirms=1, output=[ r".*The remote URL seems to require repository ownership: .*", ], @@ -239,18 +239,20 @@ def test( gen_manifest=None, expect_success=False ) - # "alr publish --for-private-index" also currently fails, but due to an - # issue with URI recognition (the host part is not properly identified). + # "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=1, + num_confirms=2, output=[ - r".* Origin is hosted on unknown site:.*", + r".*Success: Your index manifest file has been generated.*", + r".*Please upload this to the index in the xx/xxx/ subdirectory.*", ], - gen_manifest=None, - expect_success=False + gen_manifest=[ + r'.*url = "git@bitbucket\.org:/some_user/repo-name\.git".*', + ], + expect_success=True ) # A crate unsuitable for the community index because it has a diff --git a/testsuite/tests/publish/ssh-remote-origin/test.py b/testsuite/tests/publish/ssh-remote-origin/test.py index ecc69a62e..3dc33a82a 100644 --- a/testsuite/tests/publish/ssh-remote-origin/test.py +++ b/testsuite/tests/publish/ssh-remote-origin/test.py @@ -28,7 +28,7 @@ p = run_alr( "publish", "--for-private-index", url, commit,complain_on_error=False ) - assert_match(r".*Origin is hosted on unknown site: .*", p.out) + assert_match(r".*Origin is hosted on unknown site: host\.invalid.*", p.out) print("SUCCESS") From bddc37f13d4212ce725514d1426ef17981869d4a Mon Sep 17 00:00:00 2001 From: Seb M'Caw Date: Fri, 30 Aug 2024 16:44:01 +0000 Subject: [PATCH 4/9] Fix test --- testsuite/tests/publish/private-indexes/test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testsuite/tests/publish/private-indexes/test.py b/testsuite/tests/publish/private-indexes/test.py index c5617dd5f..3543199cb 100644 --- a/testsuite/tests/publish/private-indexes/test.py +++ b/testsuite/tests/publish/private-indexes/test.py @@ -93,7 +93,7 @@ def test( ], input=num_confirms * ["y"], complain_on_error=expect_success, - timeout=10, + timeout=60, ) # Check output matches From 7b68f920714e141707e7daeb0a63c6091d0593c8 Mon Sep 17 00:00:00 2001 From: Seb M'Caw Date: Mon, 2 Sep 2024 08:21:47 +0000 Subject: [PATCH 5/9] Bugfix --- src/alire/alire-uri.adb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/alire/alire-uri.adb b/src/alire/alire-uri.adb index 9715b5858..afd90830f 100644 --- a/src/alire/alire-uri.adb +++ b/src/alire/alire-uri.adb @@ -68,7 +68,11 @@ package body Alire.URI is -- 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 - return Head (Auth, "]:") & "]"; + if Contains (Auth, "]:") then + return Head (Auth, "]:") & "]"; + else + return Auth; + end if; else return Head (Auth, ":"); end if; From 947e43e8f0675a5b95b1377764e6bb20ee8e0143 Mon Sep 17 00:00:00 2001 From: Seb M'Caw Date: Tue, 24 Sep 2024 09:22:10 +0000 Subject: [PATCH 6/9] Update 'config' to 'settings' in 'alr init' --- src/alr/alr-commands-init.adb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/alr/alr-commands-init.adb b/src/alr/alr-commands-init.adb index 17f378eba..102c2f349 100644 --- a/src/alr/alr-commands-init.adb +++ b/src/alr/alr-commands-init.adb @@ -632,14 +632,14 @@ package body Alr.Commands.Init is & "requested now."); TIO.New_Line; TIO.Put_Line - ("You can edit this information at any time with 'alr config'"); + ("You can edit this information at any time with 'alr settings'"); TIO.New_Line; end if; Info.Username := To_Unbounded_String (UI.Query_Config.User_Name); Query_GitHub_Login (Info); Info.Email := To_Unbounded_String (UI.Query_Config.User_Email); - -- Make it clear that the remainder can't be changed with `alr config` + -- Make it clear that the remainder can't be changed with `alr settings` TIO.New_Line; if User_Not_Already_Configured then AAA.Text_IO.Put_Paragraph From d372bb0732dd53a0b121fe9eb504160256370f61 Mon Sep 17 00:00:00 2001 From: Seb M'Caw Date: Tue, 24 Sep 2024 09:28:35 +0000 Subject: [PATCH 7/9] Update 'config' to 'settings' elsewhere --- scripts/ci-github.sh | 2 +- src/alire/os_windows/alire-platforms-current__windows.adb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/ci-github.sh b/scripts/ci-github.sh index 53d36e9dd..0b28b2dcb 100755 --- a/scripts/ci-github.sh +++ b/scripts/ci-github.sh @@ -39,7 +39,7 @@ fi # Disable distro detection if supported if [ "${ALIRE_DISABLE_DISTRO:-}" == "true" ]; then - alr config --global --set distribution.disable_detection true + alr settings --global --set distribution.disable_detection true fi # For the record diff --git a/src/alire/os_windows/alire-platforms-current__windows.adb b/src/alire/os_windows/alire-platforms-current__windows.adb index 135519306..8ec2f3fd6 100644 --- a/src/alire/os_windows/alire-platforms-current__windows.adb +++ b/src/alire/os_windows/alire-platforms-current__windows.adb @@ -173,7 +173,7 @@ package body Alire.Platforms.Current is Trace.Detail ("Alire is configured not to install msys2."); Trace.Detail - ("Run 'alr config --global --set msys2.do_not_install false'" & + ("Run 'alr settings --global --set msys2.do_not_install false'" & " if you want Alire to install msys2."); return False; end if; From 1e48f9481d25b7d87f6af445983230bdd3742467 Mon Sep 17 00:00:00 2001 From: Seb M'Caw Date: Tue, 24 Sep 2024 09:30:25 +0000 Subject: [PATCH 8/9] Rewrite documentation --- doc/publishing.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/doc/publishing.md b/doc/publishing.md index 775412af4..23ab40ca7 100644 --- a/doc/publishing.md +++ b/doc/publishing.md @@ -317,11 +317,16 @@ Having a local or private index may be useful sometimes, be it for local testing, or for private crates not intended for publication. There is no practical difference between the community index and a private index -stored locally on disk or on your own infrastructure. An index must be an -accessible path or URL at which an `index.toml` file can be found, either at the -root or in any immediate subdirectory. This file specifies the version of the -index, containing one line with the form `version = "0.0.0"`. No files should be placed in the same -location as `index.toml` except for the manifests of published crates. +stored locally on disk or on your own infrastructure. An index must be located +in a first level subdirectory of an accessible git repository or local +filesystem location (or optionally at the top level in the case of a local +filesystem index). This subdirectory should contain only an `index.toml` +file and one or more `cr/crate_name` subdirectories within which the crate +manifests themselves are located. The `index.toml` file contains one line with +the form `version = "x.x.x"`, specifying the index format used. The range of +versions Alire is compatible with can be found by running `alr version`, and +breaking changes are listed in +[BREAKING.md](https://github.com/alire-project/alire/blob/master/BREAKING.md). To start using such an index, run @@ -340,5 +345,5 @@ Additions to indexes stored locally on the disk will take effect immediately, unless the crate being published contains `"provides"` definitions, in which case an index update will be required (either with `alr index --update-all`, or through a scheduled auto-update) to ensure it is properly used by the dependency -solver. An index update will always be required when publishing to a remote -index. +solver. An index update will always be required when publishing to a git +repository index. From 821b422ddc2ffd14fede6ecdb14a6a5efaf6e88a Mon Sep 17 00:00:00 2001 From: Seb M'Caw Date: Tue, 24 Sep 2024 09:34:28 +0000 Subject: [PATCH 9/9] Clarify upload instructions --- src/alire/alire-publish.adb | 2 +- testsuite/tests/publish/private-indexes/test.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/alire/alire-publish.adb b/src/alire/alire-publish.adb index 78d006988..b62352360 100644 --- a/src/alire/alire-publish.adb +++ b/src/alire/alire-publish.adb @@ -623,7 +623,7 @@ package body Alire.Publish is -- unknown, so we can only give generic instructions on where to -- place the file. Put_Info - ("Please upload this to the index in the " + ("Please upload this file to the index in the " & TTY.URL (String (TOML_Index.Manifest_Path (Name)) & "/") & " subdirectory."); elsif not Settings.Builtins.User_Github_Login.Is_Empty then diff --git a/testsuite/tests/publish/private-indexes/test.py b/testsuite/tests/publish/private-indexes/test.py index 3543199cb..a21265f43 100644 --- a/testsuite/tests/publish/private-indexes/test.py +++ b/testsuite/tests/publish/private-indexes/test.py @@ -175,7 +175,7 @@ def test( r".*Success: Your index manifest file has been generated.*", # alr should provide instructions again, but they should be more # generic, since we don't know where the private index is located. - r".*Please upload this to the index in the xx/xxx/ subdirectory.*", + 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".*', @@ -247,7 +247,7 @@ def test( num_confirms=2, output=[ r".*Success: Your index manifest file has been generated.*", - r".*Please upload this to the index in the xx/xxx/ subdirectory.*", + 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".*', @@ -295,7 +295,7 @@ def test( num_confirms=2, output=[ r".*Success: Your index manifest file has been generated.*", - r".*Please upload this to the index in the xx/xxx/ subdirectory.*", + 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".*', @@ -337,7 +337,7 @@ def test( num_confirms=2, output=[ r".*Success: Your index manifest file has been generated.*", - r".*Please upload this to the index in the xx/xxx/ subdirectory.*", + 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".*', @@ -379,7 +379,7 @@ def test( num_confirms=2, output=[ r".*Success: Your index manifest file has been generated.*", - r".*Please upload this to the index in the xx/xxx/ subdirectory.*", + 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".*',