From fbaa3daba0df73f73b4fc42869b3297ebace8370 Mon Sep 17 00:00:00 2001 From: Seb M'Caw Date: Tue, 24 Sep 2024 19:42:10 +0100 Subject: [PATCH] feat: support for private indexes with `alr publish --for-private-index` (#1745) * Add support for private indexes to "alr publish" * Fix tests * Support "git@" remotes * Fix test * Bugfix * Update 'config' to 'settings' in 'alr init' * Update 'config' to 'settings' elsewhere * Rewrite documentation * Clarify upload instructions --- doc/catalog-format-spec.md | 10 +- doc/publishing.md | 46 ++- scripts/ci-github.sh | 2 +- src/alire/alire-origins.adb | 5 +- 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 | 150 +++++-- 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 + src/alire/alire-uri.adb | 30 ++ src/alire/alire-uri.ads | 8 + .../alire-utils-user_input-query_config.adb | 16 +- .../alire-utils-user_input-query_config.ads | 4 +- .../alire-platforms-current__windows.adb | 2 +- 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 | 78 ++++ .../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 ++-- .../pin/branch-remote-protocols/test.yaml | 3 - .../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 | 391 ++++++++++++++++++ .../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 +- 54 files changed, 1103 insertions(+), 205 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..23ab40ca7 100644 --- a/doc/publishing.md +++ b/doc/publishing.md @@ -313,15 +313,37 @@ 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 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 + +`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 git +repository index. 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/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-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..b62352360 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 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 + -- 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); @@ -897,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; @@ -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) @@ -1034,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 -- @@ -1164,9 +1223,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 +1315,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-uri.adb b/src/alire/alire-uri.adb index 1c8e47a48..afd90830f 100644 --- a/src/alire/alire-uri.adb +++ b/src/alire/alire-uri.adb @@ -49,6 +49,36 @@ 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 + if Contains (Auth, "]:") then + return Head (Auth, "]:") & "]"; + else + return Auth; + end if; + 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/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/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; diff --git a/src/alr/alr-commands-init.adb b/src/alr/alr-commands-init.adb index 0895c203f..102c2f349 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 settings'"); + 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 settings` + 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..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 @@ -293,3 +294,80 @@ 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: + """ + NON-WINDOWS-ONLY + 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): + # 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") + ) + # Add the directory to PATH + try: + os.mkdir(self._mock_git_dir) + except FileExistsError: + pass + 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), + 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}{os.pathsep}', '', 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/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/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..a21265f43 --- /dev/null +++ b/testsuite/tests/publish/private-indexes/test.py @@ -0,0 +1,391 @@ +""" +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=60, + ) + + # 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 file 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, + 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 + ) + + # 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 file 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 file 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 file 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..3dc33a82a --- /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: host\.invalid.*", 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")