aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBoris Kolpackov <boris@codesynthesis.com>2018-08-24 13:33:01 +0200
committerKaren Arutyunov <karen@codesynthesis.com>2018-08-25 13:38:41 +0300
commitfe6aa3aa87bdff77ca667e012a9d1cc34f1fb8ea (patch)
treeaf89684406dbb6b6f13bd74e9b09cf76eb6d6ebd
parent5f85dd75c096b57a085737a8164099cb1ef19181 (diff)
Implement bdep-ci command
-rw-r--r--bdep/bdep.cli5
-rw-r--r--bdep/bdep.cxx2
-rw-r--r--bdep/buildfile2
-rw-r--r--bdep/ci.cli96
-rw-r--r--bdep/ci.cxx295
-rw-r--r--bdep/ci.hxx19
-rw-r--r--bdep/git.cxx163
-rw-r--r--bdep/git.hxx13
-rw-r--r--bdep/project.cxx67
-rw-r--r--bdep/project.hxx8
-rw-r--r--bdep/publish.cli7
-rw-r--r--bdep/publish.cxx230
-rw-r--r--bdep/types.hxx3
-rwxr-xr-xdoc/cli.sh2
-rw-r--r--tests/publish.test2
15 files changed, 700 insertions, 214 deletions
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 <bdep/sync.hxx>
#include <bdep/fetch.hxx>
#include <bdep/status.hxx>
+#include <bdep/ci.hxx>
#include <bdep/publish.hxx>
#include <bdep/deinit.hxx>
#include <bdep/config.hxx>
@@ -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 <bdep/project.cli>;
+
+"\section=1"
+"\name=bdep-ci"
+"\summary=submit project test request to CI server"
+
+namespace bdep
+{
+ {
+ "<options>
+ <prj-spec> <prj-dir>
+ <pkg-spec> <pkg-dir>
+ <cfg-spec> <cfg-name> <cfg-dir>",
+
+ "\h|SYNOPSIS|
+
+ \c{\b{bdep ci} [<options>] [<cfg-spec>] [<pkg-spec>]}
+
+ \c{<pkg-spec> = (\b{--directory}|\b{-d} <pkg-dir>)... | <prj-spec>\n
+ <prj-spec> = \b{--directory}|\b{-d} <prj-dir>\n
+ <cfg-spec> = \b{@}<cfg-name> | \b{--config}|\b{-c} <cfg-dir>}
+
+ \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"
+ {
+ "<url>",
+ "CI server to submit the request to."
+ }
+
+ url --repository
+ {
+ "<url>",
+ "Remote repository URL for the project."
+ }
+
+ string --simulate
+ {
+ "<outcome>",
+ "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 <bdep/ci.hxx>
+
+#include <bdep/git.hxx>
+#include <bdep/project.hxx>
+#include <bdep/database.hxx>
+#include <bdep/diagnostics.hxx>
+
+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 <commit> | (initial) Current commit.
+ // # branch.head <branch> | (detached) Current branch.
+ // # branch.upstream <upstream_branch> If upstream is set.
+ // # branch.ab +<ahead> -<behind> 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 <remote>/<branch> 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<configuration> 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<package> 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 <bdep/types.hxx>
+#include <bdep/utility.hxx>
+
+#include <bdep/ci-options.hxx>
+
+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<string>
+ {
+ 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<string> l = git_config (cfg))
+ {
+ return parse_url (*l, cfg);
+ }
+ }
+
+ // Next is remote.origin.build2Url.
+ //
+ if (optional<string> 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<string> 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 <host>:<path> 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<string>
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 <bdep/git.ixx>
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<package_name>;
#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 <libbutl/fdstream.mxx> // fdterm()
#include <libbutl/manifest-parser.mxx>
-#include <libbutl/standard-version.mxx>
#include <libbutl/manifest-serializer.mxx>
#include <libbpkg/manifest.hxx>
@@ -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<string> 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<string> 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 <host>:<path> 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<url> 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 <libbutl/fdstream.mxx>
#include <libbutl/small-vector.mxx>
#include <libbutl/semantic-version.mxx>
+#include <libbutl/standard-version.mxx>
namespace bdep
{
@@ -112,8 +113,10 @@ namespace bdep
using butl::fdstream_mode;
// <libbutl/semantic-version.mxx>
+ // <libbutl/standard-version.mxx>
//
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