From fe6aa3aa87bdff77ca667e012a9d1cc34f1fb8ea Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Fri, 24 Aug 2018 13:33:01 +0200 Subject: Implement bdep-ci command --- bdep/bdep.cli | 5 + bdep/bdep.cxx | 2 + bdep/buildfile | 2 + bdep/ci.cli | 96 +++++++++++++++++ bdep/ci.cxx | 295 +++++++++++++++++++++++++++++++++++++++++++++++++++++ bdep/ci.hxx | 19 ++++ bdep/git.cxx | 163 +++++++++++++++++++++++++++++ bdep/git.hxx | 13 ++- bdep/project.cxx | 67 ++++++++++++ bdep/project.hxx | 8 ++ bdep/publish.cli | 7 +- bdep/publish.cxx | 230 ++++------------------------------------- bdep/types.hxx | 3 + doc/cli.sh | 2 +- tests/publish.test | 2 +- 15 files changed, 700 insertions(+), 214 deletions(-) create mode 100644 bdep/ci.cli create mode 100644 bdep/ci.cxx create mode 100644 bdep/ci.hxx diff --git a/bdep/bdep.cli b/bdep/bdep.cli index 284f1a5..2b8b278 100644 --- a/bdep/bdep.cli +++ b/bdep/bdep.cli @@ -443,6 +443,11 @@ namespace bdep "\l{bdep-status(1)} \- print status of project and/or its dependencies" } + bool ci + { + "\l{bdep-ci(1)} \- submit project test request to CI server" + } + bool publish { "\l{bdep-publish(1)} \- publish project to archive repository" diff --git a/bdep/bdep.cxx b/bdep/bdep.cxx index f77870f..8fbcd71 100644 --- a/bdep/bdep.cxx +++ b/bdep/bdep.cxx @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -296,6 +297,7 @@ try COMMAND_IMPL (init, init, "init", true); COMMAND_IMPL (fetch, fetch, "fetch", true); COMMAND_IMPL (status, status, "status", true); + COMMAND_IMPL (ci, ci, "ci", true); COMMAND_IMPL (publish, publish, "publish", true); COMMAND_IMPL (deinit, deinit, "deinit", true); COMMAND_IMPL (config, config, "config", true); diff --git a/bdep/buildfile b/bdep/buildfile index a234d96..cc71107 100644 --- a/bdep/buildfile +++ b/bdep/buildfile @@ -27,6 +27,7 @@ init-options \ sync-options \ fetch-options \ status-options \ +ci-options \ publish-options \ deinit-options \ config-options \ @@ -71,6 +72,7 @@ if $cli.configured cli.cxx{sync-options}: cli{sync} cli.cxx{fetch-options}: cli{fetch} cli.cxx{status-options}: cli{status} + cli.cxx{ci-options}: cli{ci} cli.cxx{publish-options}: cli{publish} cli.cxx{deinit-options}: cli{deinit} cli.cxx{config-options}: cli{config} diff --git a/bdep/ci.cli b/bdep/ci.cli new file mode 100644 index 0000000..1709868 --- /dev/null +++ b/bdep/ci.cli @@ -0,0 +1,96 @@ +// file : bdep/ci.cli +// copyright : Copyright (c) 2014-2018 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +include ; + +"\section=1" +"\name=bdep-ci" +"\summary=submit project test request to CI server" + +namespace bdep +{ + { + " + + + ", + + "\h|SYNOPSIS| + + \c{\b{bdep ci} [] [] []} + + \c{ = (\b{--directory}|\b{-d} )... | \n + = \b{--directory}|\b{-d} \n + = \b{@} | \b{--config}|\b{-c} } + + \h|DESCRIPTION| + + The \cb{ci} command submits the project packages test request to a CI + server. + + If no project or package directory is specified, then the current working + directory is assumed. If no configuration is specified, then the default + configuration is used. If the specified directory is a project directory, + then all the packages initialized in the configuration are submitted. See + \l{bdep-projects-configs(1)} for details on specifying projects and + configurations. + + A CI request consists of the specified packages and their versions as + well as the project's remote version control repository URL corresponding + to the current (local) state of the project. The CI server should be able + fetch these package versions from this repository as well as any + dependencies from this repository or its prerequisites/complements + (according to \cb{repositories.manifest}). + + If the CI server is not explicitly specified with the \cb{--server} + option, the request is submitted to \cb{ci.cppget.org} by default. + + Unless the remote repository URL is specified with the \cb{--repository} + option, it will be automatically derived from the version control's + \"remote\" URL. In case of \cb{git(1)}, it will be based on the + \cb{remote.origin.url} configuration value unless overridden with + \cb{remote.origin.build2Url}. The repository URL is then adjusted to + corresponding to the current (local) state of the project. In case of + \cb{git(1)}, the current branch and commit id are added as the repository + URL fragment (see \l{bpkg-repository-types(1)} for details). + + While the exact interpretation of the CI request depends on the specific + service, normally, the CI server will respond with a reference that can + be used to query the results. See \l{brep#ci Package CI} for details on + the CI request handling. + " + } + + class cmd_ci_options: project_options + { + "\h|CI OPTIONS|" + + bool --yes|-y + { + "Don't prompt for confirmation before submitting." + } + + url --server = "https://ci.cppget.org" + { + "", + "CI server to submit the request to." + } + + url --repository + { + "", + "Remote repository URL for the project." + } + + string --simulate + { + "", + "Simulate the specified outcome of the CI process without actually + performing any externally visible actions (such as testing the packages + or publishing the result). The commonly used outcome value is + \cb{success}. For other recognized outcomes refer to the CI service + documentation." + } + }; +} diff --git a/bdep/ci.cxx b/bdep/ci.cxx new file mode 100644 index 0000000..200cffa --- /dev/null +++ b/bdep/ci.cxx @@ -0,0 +1,295 @@ +// file : bdep/ci.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2018 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include + +#include +#include +#include +#include + +using namespace std; +using namespace butl; + +namespace bdep +{ + // Get the project's remote repository URL corresponding to the current + // (local) state of the repository. Fail if the working directory is not + // clean or if the local state isn't in sync with the remote. + // + static url + git_repository_url (const cmd_ci_options& o, const dir_path& prj) + { + // This is what we need to do: + // + // 1. Check that the working directory is clean. + // + // 2. Check that we are not ahead of upstream. + // + // 3. Get the corresponding upstream branch. + // + // 4. Get the current commit id. + // + // And aren't we in luck today: git-status --porcelain=2 (available since + // git 2.11.0) gives us all this information with a single invocation. + // + string branch; + string commit; + { + string head; + string upstream; + + process pr; + bool io (false); + try + { + fdpipe pipe (fdopen_pipe ()); // Text mode seems appropriate. + + pr = start_git (semantic_version {2, 11, 0}, + prj, + 0 /* stdin */, + pipe /* stdout */, + 2 /* stderr */, + "status", + "--porcelain=2", + "--branch"); + + pipe.out.close (); + ifdstream is (move (pipe.in), fdstream_mode::skip, ifdstream::badbit); + + // Lines starting with '#' are headers with any other line indicating + // some kind of change. + // + // The headers we are interested in are: + // + // # branch.oid | (initial) Current commit. + // # branch.head | (detached) Current branch. + // # branch.upstream If upstream is set. + // # branch.ab + - If upstream is set and + // the commit is present. + // + // Note that if we are in the detached HEAD state, then we will only + // see the first two with branch.head being '(detached)'. + // + for (string l; !eof (getline (is, l)); ) + { + if (l[0] != '#') + fail << "project directory has uncommitted changes" << + info << "run 'git status' for details"; + + if (l.compare (2, 10, "branch.oid") == 0) + { + commit = string (l, 13); + + if (commit == "(initial)") + fail << "no commits in project repository" << + info << "run 'git status' for details"; + } + else if (l.compare (2, 11, "branch.head") == 0) + { + head = string (l, 14); + + if (head == "(detached)") + fail << "project directory is in the detached HEAD state" << + info << "run 'git status' for details"; + } + else if (l.compare (2, 15, "branch.upstream") == 0) + { + // This is normally in the / form, for example + // 'origin/master'. + // + upstream = string (l, 18); + size_t p (path::traits::rfind_separator (upstream)); + branch = p != string::npos ? string (upstream, p + 1) : upstream; + } + else if (l.compare (2, 9, "branch.ab") == 0) + { + // We definitely don't want to be ahead (upstream doesn't have + // this commit) but there doesn't seem be anything wrong with + // being behind. + // + if (l.compare (12, 3, "+0 ") != 0) + fail << "local branch '" << head << "' is ahead of '" + << upstream << "'" << + info << "run 'git push' to update"; + } + } + + is.close (); // Detect errors. + } + catch (const io_error&) + { + // Presumably the child process failed and issued diagnostics so let + // finish_git() try to deal with that. + // + io = true; + } + + finish_git (pr, io); + + // Make sure we've got everything we need. + // + if (commit.empty ()) + fail << "unable to obtain current commit" << + info << "run 'git status' for details"; + + if (branch.empty ()) + fail << "no upstream branch set for local branch '" << head << "'" << + info << "run 'git push --set-upstream' to set"; + } + + // We treat the URL specified with --repository as a "base", that is, we + // still add the fragment. + // + url r (o.repository_specified () + ? o.repository () + : git_remote_url (prj, "--repository")); + + if (r.fragment) + fail << "remote git repository URL '" << r << "' already has fragment"; + + // We specify both the branch and the commit to give bpkg every chance to + // minimize the amount of history to fetch (see bpkg-repository-types(1) + // for details). + // + r.fragment = branch + '@' + commit; + + return r; + } + + static url + repository_url (const cmd_ci_options& o, const dir_path& prj) + { + if (git_repository (prj)) + return git_repository_url (o, prj); + + fail << "project has no known version control-based repository" << endf; + } + + int + cmd_ci (const cmd_ci_options& o, cli::scanner&) + { + tracer trace ("ci"); + + // If we are submitting the entire project, then we have two choices: we + // can list all the packages in the project or we can only do so for + // packages that were initialized in the (specified) configuration(s?). + // + // Note that other than getting the list of packages, we would only need + // the configuration to obtain their versions. Since we can only have one + // version for each package this is not strictly necessary but is sure a + // good sanity check against local/remote mismatches. Also, it would be + // nice to print the versions we are submitting in the prompt. + // + // While this isn't as clear cut, it also feels like a configuration could + // be expected to serve as a list of packages, in case, for example, one + // has configurations for subsets of packages or some such. And in the + // future, who knows, we could have multi-project CI. + // + // So, let's go with the configuration. Specifically, if packages were + // explicitly specified, we verify they are initialized. Otherwise, we use + // the list of packages that are initialized in a configuration (single + // for now). + // + // Note also that no pre-sync is needed since we are only getting versions + // (via the info meta-operation). + // + project_packages pp ( + find_project_packages (o, + false /* ignore_packages */, + false /* load_packages */)); + + const dir_path& prj (pp.project); + database db (open (prj, trace)); + + shared_ptr cfg; + { + transaction t (db.begin ()); + configurations cfgs (find_configurations (o, prj, t)); + t.commit (); + + if (cfgs.size () > 1) + fail << "multiple configurations specified for ci"; + + // If specified, verify packages are present in the configuration. + // + if (!pp.packages.empty ()) + verify_project_packages (pp, cfgs); + + cfg = move (cfgs[0]); + } + + // Collect package names and their versions. + // + struct package + { + package_name name; + standard_version version; + }; + vector pkgs; + + auto add_package = [&o, &cfg, &pkgs] (package_name n) + { + standard_version v (package_version (o, cfg->path, n)); + pkgs.push_back (package {move (n), move (v)}); + }; + + if (pp.packages.empty ()) + { + for (const package_state& p: cfg->packages) + add_package (p.name); + } + else + { + for (package_location& p: pp.packages) + add_package (p.name); + } + + // Get the server and repository URLs. + // + const url& srv (o.server ()); + const url rep (repository_url (o, prj)); + + // Print the plan and ask for confirmation. + // + if (!o.yes ()) + { + text << "submitting:" << '\n' + << " to: " << srv << '\n' + << " in: " << rep; + + for (const package& p: pkgs) + { + diag_record dr (text); + + // If printing multiple packages, separate them with a blank line. + // + if (pkgs.size () > 1) + dr << '\n'; + + dr << " package: " << p.name << '\n' + << " version: " << p.version; + } + + if (!yn_prompt ("continue? [y/n]")) + return 1; + } + + // Submit the request. + // + { + // Print progress unless we had a prompt. + // + if (verb && o.yes ()) + text << "submitting to " << srv; + + //@@ TODO call submit() + + if (verb) + text << "@@ TODO: print response"; + } + + return 0; + } +} diff --git a/bdep/ci.hxx b/bdep/ci.hxx new file mode 100644 index 0000000..cd3e563 --- /dev/null +++ b/bdep/ci.hxx @@ -0,0 +1,19 @@ +// file : bdep/ci.hxx -*- C++ -*- +// copyright : Copyright (c) 2014-2018 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BDEP_CI_HXX +#define BDEP_CI_HXX + +#include +#include + +#include + +namespace bdep +{ + int + cmd_ci (const cmd_ci_options&, cli::scanner& args); +} + +#endif // BDEP_CI_HXX diff --git a/bdep/git.cxx b/bdep/git.cxx index a5e2be4..e9b1eba 100644 --- a/bdep/git.cxx +++ b/bdep/git.cxx @@ -85,4 +85,167 @@ namespace bdep return r; } + + url + git_remote_url (const dir_path& repo, + const char* opt, + const char* what, + const char* cfg) + { + auto git_config = [&repo] (const char* name) -> optional + { + return git_line (semantic_version {2, 1, 0}, + repo, + true /* ignore_error */, + "config", + "--get", + name); + }; + + auto parse_url = [] (const string& s, const char* w) + { + try + { + return url (s); + } + catch (const invalid_argument& e) + { + fail << "invalid " << w << " value '" << s << "': " << e << endf; + } + }; + + // First try the custom config value if specified. + // + if (cfg != nullptr) + { + if (optional l = git_config (cfg)) + { + return parse_url (*l, cfg); + } + } + + // Next is remote.origin.build2Url. + // + if (optional l = git_config ("remote.origin.build2Url")) + { + return parse_url (*l, "remote.origin.build2Url"); + } + + // Finally, get remote.origin.url and try to derive an HTTPS URL from it. + // + if (optional l = git_config ("remote.origin.url")) + { + string& s (*l); + + // This one will be fuzzy and hairy. Here are some representative + // examples of what we can encounter: + // + // example.org:/path/to/repo.git + // user@example.org:/path/to/repo.git + // user@example.org:~user/path/to/repo.git + // ssh://user@example.org/path/to/repo.git + // + // git://example.org/path/to/repo.git + // + // http://example.org/path/to/repo.git + // https://example.org/path/to/repo.git + // + // /path/to/repo.git + // file:///path/to/repo.git + // + // Note that git seem to always make remote.origin.url absolute in + // case of a local filesystem path. + // + // So the algorithm will be as follows: + // + // 1. If there is scheme, then parse as URL. + // + // 2. Otherwise, check if this is an absolute path. + // + // 3. Otherwise, assume SSH : thing. + // + url u; + + // Find the scheme. + // + // Note that in example.org:/path/... example.org is a valid scheme. To + // distinguish this, we check if the scheme contains any dots (none of + // the schemes recognized by git currently do and probably never will). + // + size_t p (s.find (':')); + if (p != string::npos && // Has ':'. + url::traits::find (s, p) == 0 && // Scheme starts at 0. + s.rfind ('.', p - 1) == string::npos) // No dots in scheme. + { + u = parse_url (s, "remote.origin.url"); + } + else + { + // Absolute path or the SSH thing. + // + if (path::traits::absolute (s)) + { + // This is what we want to end up with: + // + // file:///tmp + // file:///c:/tmp + // + const char* h (s[0] == '/' ? "file://" : "file:///"); + u = parse_url (h + s, "remote.origin.url"); + } + else if (p != string::npos) + { + // This can still include user (user@host) so let's add the scheme, + // replace/erase ':', and parse it as a string representation of a + // URL. + // + if (s[p + 1] == '/') // POSIX notation. + s.erase (p, 1); + else + s[p] = '/'; + + u = parse_url ("ssh://" + s, "remote.origin.url"); + } + else + fail << "invalid remote.origin.url value '" << s << "': not a URL"; + } + + // A remote URL gotta have authority. + // + if (u.authority) + { + if (u.scheme == "http" || u.scheme == "https") + { + // This can still include the user which we most likely don't want. + // + u.authority->user.clear (); + return u; + } + + // Derive an HTTPS URL from a remote URL (and hope for the best). + // + if (u.scheme != "file" && u.path) + return url ("https", u.authority->host, *u.path); + } + + diag_record dr (fail); + + dr << "unable to derive " << what << " from " << u; + + dr << info << "consider setting git "; + if (cfg != nullptr) + dr << cfg << " or "; + dr << "remote.origin.build2Url value"; + + if (opt != nullptr) + dr << info << "or use " << opt << " to specify explicitly"; + } + + // We don't necessarily want to suggest remote.origin.build2* or the + // option since preferably this should be derived automatically from + // remote.origin.url. + // + fail << "unable to discover " << what << ": no git remote.origin.url " + << "value" << endf; + } } diff --git a/bdep/git.hxx b/bdep/git.hxx index fcc9513..8ef9397 100644 --- a/bdep/git.hxx +++ b/bdep/git.hxx @@ -30,7 +30,7 @@ namespace bdep I&& in, O&& out, E&& err, A&&... args); - // Wait git process to terminate. + // Wait for git process to terminate. // void finish_git (process& pr, bool io_read = false); @@ -60,6 +60,17 @@ namespace bdep // optional git_line (process&& pr, fdpipe&& pipe, bool ignore_error); + + // Try to derive a remote HTTPS repository URL from the optionally specified + // custom git config value falling back to remote.origin.build2Url and then + // remote.origin.url. Issue diagnostics (including a suggestion to use + // option opt, if specified) and fail if unable to. + // + url + git_remote_url (const dir_path& repo, + const char* opt = nullptr, + const char* what = "remote repository URL", + const char* cfg = nullptr); } #include diff --git a/bdep/project.cxx b/bdep/project.cxx index cd4f029..8178b01 100644 --- a/bdep/project.cxx +++ b/bdep/project.cxx @@ -381,4 +381,71 @@ namespace bdep } } } + + standard_version + package_version (const common_options& o, + const dir_path& cfg, + const package_name& p) + { + // We could have used bpkg-pkg-status but then we would have to deal with + // iterations. So we use the build system's info meta-operation directly. + // + string v; + { + process pr; + bool io (false); + try + { + fdpipe pipe (fdopen_pipe ()); // Text mode seems appropriate. + + // Note: the package directory inside the configuration is a bit of an + // assumption. + // + pr = start_b ( + o, + pipe /* stdout */, + 2 /* stderr */, + "info:", (dir_path (cfg) /= p.string ()).representation ()); + + pipe.out.close (); + ifdstream is (move (pipe.in), fdstream_mode::skip, ifdstream::badbit); + + for (string l; !eof (getline (is, l)); ) + { + // Verify the name for good measure (comes before version). + // + if (l.compare (0, 9, "project: ") == 0) + { + if (l.compare (9, string::npos, p.string ()) != 0) + fail << "name mismatch for package " << p; + } + else if (l.compare (0, 9, "version: ") == 0) + { + v = string (l, 9); + break; + } + } + + is.close (); // Detect errors. + } + catch (const io_error&) + { + // Presumably the child process failed and issued diagnostics so let + // finish_b() try to deal with that first. + // + io = true; + } + + finish_b (o, pr, io); + } + + try + { + return standard_version (v); + } + catch (const invalid_argument& e) + { + fail << "invalid package " << p << " version " << v << ": " << e << endf; + } + } } diff --git a/bdep/project.hxx b/bdep/project.hxx index 4047dc5..e508374 100644 --- a/bdep/project.hxx +++ b/bdep/project.hxx @@ -43,6 +43,7 @@ namespace bdep // package_name // using bpkg::package_name; + using package_names = vector; #pragma db value(package_name) type("TEXT") options("COLLATE NOCASE") @@ -219,6 +220,13 @@ namespace bdep // void verify_project_packages (const project_packages&, const configurations&); + + // Determine the version of a package in the specified configuration. + // + standard_version + package_version (const common_options&, + const dir_path& cfg, + const package_name&); } #endif // BDEP_PROJECT_HXX diff --git a/bdep/publish.cli b/bdep/publish.cli index bcbb022..53e7916 100644 --- a/bdep/publish.cli +++ b/bdep/publish.cli @@ -26,7 +26,7 @@ namespace bdep \h|DESCRIPTION| - The \cb{publish} command published the project packages to an + The \cb{publish} command publishes the project packages to an archive-based repository. If no project or package directory is specified, then the current working @@ -78,8 +78,9 @@ namespace bdep option, it will be automatically derived from the version control's \"remote\" URL. In case of \cb{git(1)}, it will be based on the \cb{remote.origin.url} configuration value unless overridden with - \cb{remote.origin.build2ControlUrl}. The special \cb{none} value to the - \cb{--control} option can be used to disable this functionality. + \cb{remote.origin.build2ControlUrl} or \cb{remote.origin.build2Url}. The + special \cb{none} value to the \cb{--control} option can be used to + disable this functionality. See \l{brep#submit Package Submission} for details on the submission request handling by archive repositories. diff --git a/bdep/publish.cxx b/bdep/publish.cxx index 22c7ab7..81afcdb 100644 --- a/bdep/publish.cxx +++ b/bdep/publish.cxx @@ -8,7 +8,6 @@ #include // fdterm() #include -#include #include #include @@ -27,23 +26,10 @@ using namespace butl; namespace bdep { // The minimum supported git version must be at least 2.5.0 due to the git - // worktree command used. We also use bpkg that caps the git version at - // 2.12.0, so let's use is as the lowest common denominator. + // worktree command used. However, there were quite a few bugs in the early + // implementation so let's cap it at a more recent and widely used 2.11.0. // - static const semantic_version git_ver {2, 12, 0}; - - static inline url - parse_url (const string& s, const char* what) - { - try - { - return url (s); - } - catch (const invalid_argument& e) - { - fail << "invalid " << what << " value '" << s << "': " << e << endf; - } - }; + static const semantic_version git_ver {2, 11, 0}; // Get the project's control repository URL. // @@ -53,193 +39,18 @@ namespace bdep if (git_repository (prj)) { // First try remote.origin.build2ControlUrl which can be used to specify - // a custom URL (e.g., if a correct one cannot be automatically derived - // from remote.origin.url). - // - if (optional l = git_line (git_ver, - prj, - true /* ignore_error */, - "config", - "--get", - "remote.origin.build2ControlUrl")) - { - return parse_url (*l, "remote.origin.build2ControlUrl"); - } - - // Otherwise, get remote.origin.url and try to derive an HTTPS URL from - // it. + // a completely different control repository URL. // - if (optional l = git_line (git_ver, - prj, - true /* ignore_error */, - "config", - "--get", - "remote.origin.url")) - { - string& s (*l); - - // This one will be fuzzy and hairy. Here are some representative - // examples of what we can encounter: - // - // example.org:/path/to/repo.git - // user@example.org:/path/to/repo.git - // user@example.org:~user/path/to/repo.git - // ssh://user@example.org/path/to/repo.git - // - // git://example.org/path/to/repo.git - // - // http://example.org/path/to/repo.git - // https://example.org/path/to/repo.git - // - // /path/to/repo.git - // file:///path/to/repo.git - // - // Note that git seem to always make remote.origin.url absolute in - // case of a local filesystem path. - // - // So the algorithm will be as follows: - // - // 1. If there is scheme, then parse as URL. - // - // 2. Otherwise, check if this is an absolute path. - // - // 3. Otherwise, assume SSH : thing. - // - url u; - - // Find the scheme. - // - // Note that in example.org:/path/... example.org is a valid scheme. - // To distinguish this, we check if the scheme contains any dots (none - // of the schemes recognized by git currently do and probably never - // will). - // - size_t p (s.find (':')); - if (p != string::npos && // Has ':'. - url::traits::find (s, p) == 0 && // Scheme starts at 0. - s.rfind ('.', p - 1) == string::npos) // No dots in scheme. - { - u = parse_url (s, "remote.origin.url"); - } - else - { - // Absolute path or the SSH thing. - // - if (path::traits::absolute (s)) - { - // This is what we want to end up with: - // - // file:///tmp - // file:///c:/tmp - // - const char* h (s[0] == '/' ? "file://" : "file:///"); - u = parse_url (h + s, "remote.origin.url"); - } - else if (p != string::npos) - { - // This can still include user (user@host) so let's add the - // scheme, replace/erase ':', and parse it as a string - // representation of a URL. - // - if (s[p + 1] == '/') // POSIX notation. - s.erase (p, 1); - else - s[p] = '/'; - - u = parse_url ("ssh://" + s, "remote.origin.url"); - } - else - fail << "invalid remote.origin.url value '" << s << "': not a URL"; - } - - if (u.scheme == "http" || u.scheme == "https") - return u; - - // Derive an HTTPS URL from a remote URL (and hope for the best). - // - if (u.scheme != "file" && u.authority && u.path) - return url ("https", u.authority->host, *u.path); - - fail << "unable to derive control repository URL from " << u << - info << "consider setting remote.origin.build2ControlUrl" << - info << "or use --control to specify explicitly"; - } - - fail << "unable to discover control repository URL: no git " - << "remote.origin.url value"; + return git_remote_url (prj, + "--control", + "control repository URL", + "remote.origin.build2ControlUrl"); } fail << "unable to discover control repository URL" << info << "use --control to specify explicitly" << endf; } - static standard_version - package_version (const common_options& o, - const dir_path& cfg, - const package_name& p) - { - // We could have used bpkg-pkg-status but then we would have to deal with - // iterations. So we use the build system's info meta-operation directly. - // - string v; - { - process pr; - bool io (false); - try - { - fdpipe pipe (fdopen_pipe ()); // Text mode seems appropriate. - - // Note: the package directory inside the configuration is a bit of an - // assumption. - // - pr = start_b ( - o, - pipe /* stdout */, - 2 /* stderr */, - "info:", (dir_path (cfg) /= p.string ()).representation ()); - - pipe.out.close (); - ifdstream is (move (pipe.in), fdstream_mode::skip, ifdstream::badbit); - - for (string l; !eof (getline (is, l)); ) - { - // Verify the name for good measure (comes before version). - // - if (l.compare (0, 9, "project: ") == 0) - { - if (l.compare (9, string::npos, p.string ()) != 0) - fail << "name mismatch for package " << p; - } - else if (l.compare (0, 9, "version: ") == 0) - { - v = string (l, 9); - break; - } - } - - is.close (); // Detect errors. - } - catch (const io_error&) - { - // Presumably the child process failed and issued diagnostics so let - // finish_b() try to deal with that first. - // - io = true; - } - - finish_b (o, pr, io); - } - - try - { - return standard_version (v); - } - catch (const invalid_argument& e) - { - fail << "invalid package " << p << " version " << v << ": " << e << endf; - } - } - // Submit package archive using the curl program and parse the response // manifest. On success, return the submission reference (first) and message // (second). Issue diagnostics and fail if anything goes wrong. @@ -707,13 +518,19 @@ namespace bdep // Control repository URL. // optional ctrl; - if (o.control_specified ()) + if (!o.control_specified ()) { - if (o.control () != "none") - ctrl = parse_url (o.control (), "--control option"); - } - else ctrl = control_url (prj); + } + else if (o.control () != "none") + try + { + ctrl = url (o.control ()); + } + catch (const invalid_argument& e) + { + fail << "invalid --control option value '" << o.control () << "': " << e; + } // Publisher's name/email. // @@ -794,18 +611,15 @@ namespace bdep { text << "publishing:" << '\n' << " to: " << repo << '\n' - << " as: " << *author.name << " <" << *author.email << '>' - << '\n'; + << " as: " << *author.name << " <" << *author.email << '>'; - for (size_t i (0); i != pkgs.size (); ++i) + for (const package& p: pkgs) { - const package& p (pkgs[i]); - diag_record dr (text); // If printing multiple packages, separate them with a blank line. // - if (i != 0) + if (pkgs.size () > 1) dr << '\n'; // Currently the control repository is the same for all packages, but diff --git a/bdep/types.hxx b/bdep/types.hxx index 7cf5c41..d4c02ee 100644 --- a/bdep/types.hxx +++ b/bdep/types.hxx @@ -29,6 +29,7 @@ #include #include #include +#include namespace bdep { @@ -112,8 +113,10 @@ namespace bdep using butl::fdstream_mode; // + // // using butl::semantic_version; + using butl::standard_version; } // In order to be found (via ADL) these have to be either in std:: or in diff --git a/doc/cli.sh b/doc/cli.sh index fc44888..6faea62 100755 --- a/doc/cli.sh +++ b/doc/cli.sh @@ -62,7 +62,7 @@ o="--suppress-undocumented --output-prefix bdep- --class-doc bdep::common_option compile "common" $o --output-suffix "-options" --class-doc bdep::common_options=long compile "bdep" $o --output-prefix "" --class-doc bdep::commands=short --class-doc bdep::topics=short -pages="new help init sync fetch status publish deinit config test update \ +pages="new help init sync fetch status ci publish deinit config test update \ clean projects-configs" for p in $pages; do diff --git a/tests/publish.test b/tests/publish.test index 63faf9c..4756abc 100644 --- a/tests/publish.test +++ b/tests/publish.test @@ -8,7 +8,7 @@ # then the default 2.1.0 (see bdep/publish.cxx for details). # +if! ($git_version_major > 2 || \ - $git_version_major == 2 && $git_version_minor >= 12) + $git_version_major == 2 && $git_version_minor >= 11) exit end -- cgit v1.1