#include "./pkg_repo_err_handle.hpp" | #include "./pkg_repo_err_handle.hpp" | ||||
#include "../options.hpp" | |||||
#include <dds/dym.hpp> | |||||
#include <dds/error/errors.hpp> | |||||
#include <dds/pkg/remote.hpp> | |||||
#include <dds/util/http/pool.hpp> | #include <dds/util/http/pool.hpp> | ||||
#include <dds/util/log.hpp> | #include <dds/util/log.hpp> | ||||
#include <dds/util/result.hpp> | #include <dds/util/result.hpp> | ||||
conn.port, | conn.port, | ||||
e.message); | e.message); | ||||
return 1; | return 1; | ||||
}, | |||||
[](matchv<pkg_repo_subcommand::remove>, | |||||
user_error<errc::no_catalog_remote_info>, | |||||
e_remote_name reponame, | |||||
dds::e_did_you_mean dym) { | |||||
dds_log(error, | |||||
"Cannot delete remote '{}', as no such remote repository is locally " | |||||
"registered by that name.", | |||||
reponame.value); | |||||
dym.log_as_error(); | |||||
write_error_marker("repo-rm-no-such-repo"); | |||||
return 1; | |||||
}); | }); | ||||
} | } |
#include "../options.hpp" | |||||
#include "./pkg_repo_err_handle.hpp" | |||||
#include <dds/pkg/db.hpp> | |||||
#include <dds/pkg/remote.hpp> | |||||
#include <dds/util/result.hpp> | |||||
namespace dds::cli::cmd { | |||||
static int _pkg_repo_remove(const options& opts) { | |||||
auto cat = opts.open_pkg_db(); | |||||
for (auto&& rm_name : opts.pkg.repo.remove.names) { | |||||
dds::remove_remote(cat, rm_name); | |||||
} | |||||
return 0; | |||||
} | |||||
int pkg_repo_remove(const options& opts) { | |||||
return handle_pkg_repo_remote_errors([&] { | |||||
DDS_E_SCOPE(opts.pkg.repo.subcommand); | |||||
return _pkg_repo_remove(opts); | |||||
}); | |||||
} | |||||
} // namespace dds::cli::cmd |
command pkg_repo_add; | command pkg_repo_add; | ||||
command pkg_repo_update; | command pkg_repo_update; | ||||
command pkg_repo_ls; | command pkg_repo_ls; | ||||
command pkg_repo_remove; | |||||
command repoman_add; | command repoman_add; | ||||
command repoman_import; | command repoman_import; | ||||
command repoman_init; | command repoman_init; | ||||
return cmd::pkg_import(opts); | return cmd::pkg_import(opts); | ||||
case pkg_subcommand::repo: | case pkg_subcommand::repo: | ||||
switch (opts.pkg.repo.subcommand) { | switch (opts.pkg.repo.subcommand) { | ||||
case cli_pkg_repo_subcommand::add: | |||||
case pkg_repo_subcommand::add: | |||||
return cmd::pkg_repo_add(opts); | return cmd::pkg_repo_add(opts); | ||||
case cli_pkg_repo_subcommand::update: | |||||
case pkg_repo_subcommand::update: | |||||
return cmd::pkg_repo_update(opts); | return cmd::pkg_repo_update(opts); | ||||
case cli_pkg_repo_subcommand::ls: | |||||
case pkg_repo_subcommand::ls: | |||||
return cmd::pkg_repo_ls(opts); | return cmd::pkg_repo_ls(opts); | ||||
case cli_pkg_repo_subcommand::_none_:; | |||||
case pkg_repo_subcommand::remove: | |||||
return cmd::pkg_repo_remove(opts); | |||||
case pkg_repo_subcommand::_none_:; | |||||
} | } | ||||
neo::unreachable(); | neo::unreachable(); | ||||
case pkg_subcommand::_none_:; | case pkg_subcommand::_none_:; |
.action = put_into(opts.if_exists), | .action = put_into(opts.if_exists), | ||||
}; | }; | ||||
argument if_missing_arg{ | |||||
.long_spellings = {"if-missing"}, | |||||
.help = "What to do if the resource does not exist", | |||||
.valname = "{fail,ignore}", | |||||
.action = put_into(opts.if_missing), | |||||
}; | |||||
argument toolchain_arg{ | argument toolchain_arg{ | ||||
.long_spellings = {"toolchain"}, | .long_spellings = {"toolchain"}, | ||||
.short_spellings = {"t"}, | .short_spellings = {"t"}, | ||||
.name = "add", | .name = "add", | ||||
.help = "Add a package repository", | .help = "Add a package repository", | ||||
})); | })); | ||||
setup_pkg_repo_remove_cmd(pkg_repo_grp.add_parser({ | |||||
.name = "remove", | |||||
.help = "Remove one or more package repositories", | |||||
})); | |||||
pkg_repo_grp.add_parser({ | pkg_repo_grp.add_parser({ | ||||
.name = "update", | .name = "update", | ||||
}); | }); | ||||
} | } | ||||
void setup_pkg_repo_remove_cmd(argument_parser& pkg_repo_remove_cmd) noexcept { | |||||
pkg_repo_remove_cmd.add_argument({ | |||||
.help = "Name of one or more repositories to remove", | |||||
.valname = "<repo-name>", | |||||
.can_repeat = true, | |||||
.action = push_back_onto(opts.pkg.repo.remove.names), | |||||
}); | |||||
pkg_repo_remove_cmd.add_argument(if_missing_arg.dup()).help | |||||
= "What to do if any of the named repositories do not exist"; | |||||
} | |||||
void setup_sdist_cmd(argument_parser& sdist_cmd) noexcept { | void setup_sdist_cmd(argument_parser& sdist_cmd) noexcept { | ||||
auto& sdist_grp = sdist_cmd.add_subparsers({ | auto& sdist_grp = sdist_cmd.add_subparsers({ | ||||
.valname = "<sdist-subcommand>", | .valname = "<sdist-subcommand>", |
/** | /** | ||||
* @brief 'dds pkg repo' subcommands | * @brief 'dds pkg repo' subcommands | ||||
*/ | */ | ||||
enum class cli_pkg_repo_subcommand { | |||||
enum class pkg_repo_subcommand { | |||||
_none_, | _none_, | ||||
add, | add, | ||||
remove, | |||||
update, | update, | ||||
ls, | ls, | ||||
}; | }; | ||||
ignore, | ignore, | ||||
}; | }; | ||||
enum class if_missing { | |||||
fail, | |||||
ignore, | |||||
}; | |||||
/** | /** | ||||
* @brief Complete aggregate of all dds command-line options, and some utilities | * @brief Complete aggregate of all dds command-line options, and some utilities | ||||
*/ | */ | ||||
// Shared `--if-exists` argument: | // Shared `--if-exists` argument: | ||||
cli::if_exists if_exists = cli::if_exists::fail; | cli::if_exists if_exists = cli::if_exists::fail; | ||||
// Shared '--if-missing' argument: | |||||
cli::if_missing if_missing = cli::if_missing::fail; | |||||
/** | /** | ||||
* @brief Open the package pkg_db based on the user-specified options. | * @brief Open the package pkg_db based on the user-specified options. | ||||
*/ | */ | ||||
struct { | struct { | ||||
/// The 'pkg repo' subcommand | /// The 'pkg repo' subcommand | ||||
cli_pkg_repo_subcommand subcommand; | |||||
pkg_repo_subcommand subcommand; | |||||
/** | /** | ||||
* @brief Parameters of 'dds pkg repo add' | * @brief Parameters of 'dds pkg repo add' | ||||
/// Whether we should update repo data after adding the repository | /// Whether we should update repo data after adding the repository | ||||
bool update = true; | bool update = true; | ||||
} add; | } add; | ||||
/** | |||||
* @brief Parameters of 'dds pkg repo remove' | |||||
*/ | |||||
struct { | |||||
/// Repositories to remove (by name) | |||||
std::vector<string> names; | |||||
} remove; | |||||
} repo; | } repo; | ||||
/** | /** |
#include <dds/dym.hpp> | #include <dds/dym.hpp> | ||||
#include <dds/error/errors.hpp> | #include <dds/error/errors.hpp> | ||||
#include <dds/util/log.hpp> | |||||
#include <range/v3/algorithm/min_element.hpp> | #include <range/v3/algorithm/min_element.hpp> | ||||
#include <range/v3/view/cartesian_product.hpp> | #include <range/v3/view/cartesian_product.hpp> | ||||
return matrix.back().back(); | return matrix.back().back(); | ||||
} | } | ||||
void dds::e_did_you_mean::log_as_error() const noexcept { | |||||
if (value) { | |||||
dds_log(error, " (Did you mean \"{}\"?)", *value); | |||||
} | |||||
} |
std::size_t lev_edit_distance(std::string_view a, std::string_view b) noexcept; | std::size_t lev_edit_distance(std::string_view a, std::string_view b) noexcept; | ||||
struct e_did_you_mean { | |||||
std::optional<std::string> value; | |||||
void log_as_error() const noexcept; | |||||
}; | |||||
class dym_target { | class dym_target { | ||||
std::optional<std::string> _candidate; | std::optional<std::string> _candidate; | ||||
dym_target* _tls_prev = nullptr; | dym_target* _tls_prev = nullptr; | ||||
auto& candidate() const noexcept { return _candidate; } | auto& candidate() const noexcept { return _candidate; } | ||||
auto e_value() const noexcept { return e_did_you_mean{_candidate}; } | |||||
std::string sentence_suffix() const noexcept { | std::string sentence_suffix() const noexcept { | ||||
if (_candidate) { | if (_candidate) { | ||||
return " (Did you mean '" + *_candidate + "'?)"; | return " (Did you mean '" + *_candidate + "'?)"; | ||||
return did_you_mean(given, ranges::views::all(strings)); | return did_you_mean(given, ranges::views::all(strings)); | ||||
} | } | ||||
template <typename Range> | |||||
e_did_you_mean calc_e_did_you_mean(std::string_view given, Range&& strings) noexcept { | |||||
return {did_you_mean(given, strings)}; | |||||
} | |||||
inline e_did_you_mean calc_e_did_you_mean(std::string_view given, | |||||
std::initializer_list<std::string_view> il) noexcept { | |||||
return calc_e_did_you_mean(given, ranges::views::all(il)); | |||||
} | |||||
} // namespace dds | } // namespace dds |
case errc::invalid_catalog_json: | case errc::invalid_catalog_json: | ||||
return "invalid-catalog-json.html"; | return "invalid-catalog-json.html"; | ||||
case errc::no_catalog_remote_info: | case errc::no_catalog_remote_info: | ||||
return "no-catalog-remote-info.html"; | |||||
return "no-pkg-remote.html"; | |||||
case errc::git_clone_failure: | case errc::git_clone_failure: | ||||
return "git-clone-failure.html"; | return "git-clone-failure.html"; | ||||
case errc::invalid_remote_url: | case errc::invalid_remote_url: | ||||
)"; | )"; | ||||
case errc::no_catalog_remote_info: | case errc::no_catalog_remote_info: | ||||
return R"( | return R"( | ||||
The catalog entry requires information regarding the remote acquisition method. | |||||
Refer to the documentation for details. | |||||
There is no package remote with the given name | |||||
)"; | )"; | ||||
case errc::git_clone_failure: | case errc::git_clone_failure: | ||||
return R"( | return R"( | ||||
case errc::invalid_catalog_json: | case errc::invalid_catalog_json: | ||||
return "The given catalog JSON data is not valid"; | return "The given catalog JSON data is not valid"; | ||||
case errc::no_catalog_remote_info: | case errc::no_catalog_remote_info: | ||||
return "The catalog JSON is missing remote acquisition information for one or more\n" | |||||
"packages"; | |||||
return "Tne named remote does not exist." BUG_STRING_SUFFIX; | |||||
case errc::git_clone_failure: | case errc::git_clone_failure: | ||||
return "A git-clone operation failed."; | return "A git-clone operation failed."; | ||||
case errc::invalid_remote_url: | case errc::invalid_remote_url: |
#include "./remote.hpp" | #include "./remote.hpp" | ||||
#include <dds/dym.hpp> | |||||
#include <dds/error/errors.hpp> | #include <dds/error/errors.hpp> | ||||
#include <dds/pkg/db.hpp> | |||||
#include <dds/temp.hpp> | #include <dds/temp.hpp> | ||||
#include <dds/util/http/pool.hpp> | #include <dds/util/http/pool.hpp> | ||||
#include <dds/util/log.hpp> | #include <dds/util/log.hpp> | ||||
#include <neo/url.hpp> | #include <neo/url.hpp> | ||||
#include <neo/utility.hpp> | #include <neo/utility.hpp> | ||||
#include <range/v3/range/conversion.hpp> | #include <range/v3/range/conversion.hpp> | ||||
#include <range/v3/view/transform.hpp> | |||||
using namespace dds; | using namespace dds; | ||||
namespace nsql = neo::sqlite3; | namespace nsql = neo::sqlite3; | ||||
dds_log(info, "Recompacting database..."); | dds_log(info, "Recompacting database..."); | ||||
db.exec("VACUUM"); | db.exec("VACUUM"); | ||||
} | } | ||||
void dds::remove_remote(pkg_db& pkdb, std::string_view name) { | |||||
auto& db = pkdb.database(); | |||||
neo::sqlite3::transaction_guard tr{db}; | |||||
auto get_rowid_st = db.prepare("SELECT remote_id FROM dds_pkg_remotes WHERE name = ?"); | |||||
get_rowid_st.bindings()[1] = name; | |||||
auto row = neo::sqlite3::unpack_single_opt<std::int64_t>(get_rowid_st); | |||||
if (!row) { | |||||
auto calc_dym = [&] { | |||||
auto all_st = db.prepare("SELECT name FROM dds_pkg_remotes"); | |||||
auto tups = neo::sqlite3::iter_tuples<std::string>(all_st); | |||||
auto names = tups | ranges::views::transform([](auto&& tup) { | |||||
auto&& [n] = tup; | |||||
return n; | |||||
}) | |||||
| ranges::to_vector; | |||||
return calc_e_did_you_mean(name, names); | |||||
}; | |||||
BOOST_LEAF_THROW_EXCEPTION(make_user_error<errc::no_catalog_remote_info>( | |||||
"There is no remote with name '{}'", name), | |||||
DDS_E_ARG(e_remote_name{std::string(name)}), | |||||
calc_dym); | |||||
} | |||||
auto [rowid] = *row; | |||||
neo::sqlite3::exec(db.prepare("DELETE FROM dds_pkg_remotes WHERE remote_id = ?"), rowid); | |||||
} |
namespace dds { | namespace dds { | ||||
class pkg_db; | |||||
struct e_remote_name { | |||||
std::string value; | |||||
}; | |||||
class pkg_remote { | class pkg_remote { | ||||
std::string _name; | std::string _name; | ||||
neo::url _base_url; | neo::url _base_url; | ||||
}; | }; | ||||
void update_all_remotes(neo::sqlite3::database_ref); | void update_all_remotes(neo::sqlite3::database_ref); | ||||
void remove_remote(pkg_db& db, std::string_view name); | |||||
} // namespace dds | } // namespace dds |
#pragma once | #pragma once | ||||
#include <boost/leaf/handle_error.hpp> | |||||
#include <boost/leaf/on_error.hpp> | #include <boost/leaf/on_error.hpp> | ||||
#include <boost/leaf/result.hpp> | #include <boost/leaf/result.hpp> | ||||
#include <neo/concepts.hpp> | #include <neo/concepts.hpp> | ||||
return res ? res.value() : static_cast<T>(arg); | return res ? res.value() : static_cast<T>(arg); | ||||
} | } | ||||
template <auto Val> | |||||
using matchv = boost::leaf::match<decltype(Val), Val>; | |||||
/** | /** | ||||
* @brief Error object representing a captured system_error exception | * @brief Error object representing a captured system_error exception | ||||
*/ | */ |
from dds_ci.dds import DDSWrapper | from dds_ci.dds import DDSWrapper | ||||
from dds_ci.testing import Project, RepoFixture | |||||
from dds_ci.testing import Project, RepoFixture, PackageJSON | |||||
from dds_ci.testing.error import expect_error_marker | |||||
def test_pkg_get(http_repo: RepoFixture, tmp_project: Project) -> None: | |||||
http_repo.import_json_data({ | |||||
'packages': { | |||||
'neo-sqlite3': { | |||||
'0.3.0': { | |||||
'remote': { | |||||
'git': { | |||||
'url': 'https://github.com/vector-of-bool/neo-sqlite3.git', | |||||
'ref': '0.3.0', | |||||
} | |||||
NEO_SQLITE_PKG_JSON = { | |||||
'packages': { | |||||
'neo-sqlite3': { | |||||
'0.3.0': { | |||||
'remote': { | |||||
'git': { | |||||
'url': 'https://github.com/vector-of-bool/neo-sqlite3.git', | |||||
'ref': '0.3.0', | |||||
} | } | ||||
} | } | ||||
} | } | ||||
} | } | ||||
}) | |||||
} | |||||
} | |||||
def test_pkg_get(http_repo: RepoFixture, tmp_project: Project) -> None: | |||||
http_repo.import_json_data(NEO_SQLITE_PKG_JSON) | |||||
tmp_project.dds.repo_add(http_repo.url) | tmp_project.dds.repo_add(http_repo.url) | ||||
tmp_project.dds.pkg_get('neo-sqlite3@0.3.0') | tmp_project.dds.pkg_get('neo-sqlite3@0.3.0') | ||||
assert tmp_project.root.joinpath('neo-sqlite3@0.3.0').is_dir() | assert tmp_project.root.joinpath('neo-sqlite3@0.3.0').is_dir() | ||||
def test_pkg_repo(http_repo: RepoFixture, tmp_project: Project) -> None: | def test_pkg_repo(http_repo: RepoFixture, tmp_project: Project) -> None: | ||||
dds = tmp_project.dds | dds = tmp_project.dds | ||||
dds.repo_add(http_repo.url) | dds.repo_add(http_repo.url) | ||||
dds.run(['pkg', 'repo', 'ls']) | |||||
dds.run(['pkg', 'repo', dds.catalog_path_arg, 'ls']) | |||||
def test_pkg_repo_rm(http_repo: RepoFixture, tmp_project: Project) -> None: | |||||
http_repo.import_json_data(NEO_SQLITE_PKG_JSON) | |||||
dds = tmp_project.dds | |||||
dds.repo_add(http_repo.url) | |||||
# Okay: | |||||
tmp_project.dds.pkg_get('neo-sqlite3@0.3.0') | |||||
# Remove the repo: | |||||
dds.run(['pkg', dds.catalog_path_arg, 'repo', 'ls']) | |||||
dds.repo_remove(http_repo.repo_name) | |||||
# Cannot double-remove a repo: | |||||
with expect_error_marker('repo-rm-no-such-repo'): | |||||
dds.repo_remove(http_repo.repo_name) | |||||
# Now, fails: | |||||
with expect_error_marker('pkg-get-no-pkg-id-listing'): | |||||
tmp_project.dds.pkg_get('neo-sqlite3@0.3.0') |
def repo_add(self, url: str) -> None: | def repo_add(self, url: str) -> None: | ||||
self.run(['pkg', 'repo', 'add', self.catalog_path_arg, url]) | self.run(['pkg', 'repo', 'add', self.catalog_path_arg, url]) | ||||
def repo_remove(self, name: str) -> None: | |||||
self.run(['pkg', 'repo', 'remove', self.catalog_path_arg, name]) | |||||
def repo_import(self, sdist: Path) -> None: | def repo_import(self, sdist: Path) -> None: | ||||
self.run(['repo', self.repo_dir_arg, 'import', sdist]) | self.run(['repo', self.repo_dir_arg, 'import', sdist]) | ||||
import subprocess | import subprocess | ||||
import pytest | import pytest | ||||
from _pytest.fixtures import FixtureRequest | |||||
class DirectoryServingHTTPRequestHandler(SimpleHTTPRequestHandler): | class DirectoryServingHTTPRequestHandler(SimpleHTTPRequestHandler): | ||||
""" | """ | ||||
A fixture handle to a dds HTTP repository, including a path and URL. | A fixture handle to a dds HTTP repository, including a path and URL. | ||||
""" | """ | ||||
def __init__(self, dds_exe: Path, info: ServerInfo) -> None: | |||||
def __init__(self, dds_exe: Path, info: ServerInfo, repo_name: str) -> None: | |||||
self.repo_name = repo_name | |||||
self.server = info | self.server = info | ||||
self.url = info.base_url | self.url = info.base_url | ||||
self.dds_exe = dds_exe | self.dds_exe = dds_exe | ||||
@pytest.fixture() | @pytest.fixture() | ||||
def http_repo(dds_exe: Path, http_tmp_dir_server: ServerInfo) -> Iterator[RepoFixture]: | |||||
def http_repo(dds_exe: Path, http_tmp_dir_server: ServerInfo, request: FixtureRequest) -> Iterator[RepoFixture]: | |||||
""" | """ | ||||
Fixture that creates a new empty dds repository and an HTTP server to serve | Fixture that creates a new empty dds repository and an HTTP server to serve | ||||
it. | it. | ||||
""" | """ | ||||
subprocess.check_call([str(dds_exe), 'repoman', 'init', str(http_tmp_dir_server.root)]) | |||||
yield RepoFixture(dds_exe, http_tmp_dir_server) | |||||
name = f'test-repo-{request.function.__name__}' | |||||
subprocess.check_call([str(dds_exe), 'repoman', 'init', str(http_tmp_dir_server.root), f'--name={name}']) | |||||
yield RepoFixture(dds_exe, http_tmp_dir_server, repo_name=name) |