// file : bpkg/rep-remove.cxx -*- C++ -*- // copyright : Copyright (c) 2014-2017 Code Synthesis Ltd // license : MIT; see accompanying LICENSE file #include #include #include // find() #include #include #include #include #include using namespace std; namespace bpkg { // Return true if the repository is reachable from the root repository via // the complements or prerequisites chains, recursively. // // Note that we can end up with a repository dependency cycle via // prerequisites. Thus we need to make sure that the repository was not // traversed yet. // using repositories = set>, compare_reference_target>; static bool reachable (database& db, const shared_ptr& r, repositories& traversed) { const string& nm (r->name); assert (!nm.empty ()); // Can't be the root repository. // We will go upstream until reach the root or traverse through all of the // dependent repositories. // if (!traversed.insert (r).second) // We have already been here. return false; for (const auto& rc: db.query ( query::complement::name == nm)) { const shared_ptr& r (rc); if (r->name.empty () /* Root? */ || reachable (db, r, traversed)) return true; } for (const auto& rd: db.query ( query::prerequisite::name == nm)) { // Note that the root repository has no prerequisites. // if (reachable (db, rd, traversed)) return true; } return false; } static inline bool reachable (database& db, const shared_ptr& r) { repositories traversed; return reachable (db, r, traversed); } // Remove a repository if it is not reachable from the root (and thus is not // required by any user-added repository). // static void rep_remove (const dir_path& c, database& db, const shared_ptr& r) { const string& nm (r->name); assert (!nm.empty ()); // Can't be the root repository. if (reachable (db, r)) return; // Remove this repository from locations of the available packages it // contains. Remove packages that come from only this repository. // for (const auto& rp: db.query ( query::repository::name == nm)) { const shared_ptr& p (rp); vector& ls (p->locations); for (auto i (ls.cbegin ()), e (ls.cend ()); i != e; ++i) { if (i->repository.object_id () == nm) { ls.erase (i); break; } } if (ls.empty ()) db.erase (p); else db.update (p); } // Cleanup the repository state if present. // // Note that this step is irreversible on failure. If something goes wrong // we will end up with a state-less fetched repository and the // configuration will be broken. Though, this in unlikely to happen, so // we will not bother for now. // // An alternative approach would be to collect all such directories and // then remove them after committing the transaction. Though, we still may // fail in the middle due to the filesystem error. // dir_path d (repository_state (r->location)); if (!d.empty ()) { dir_path sd (c / repos_dir / d); if (exists (sd)) rm_r (sd); } // Note that it is essential to erase the repository object from the // database prior to its complements and prerequisites removal as they // must be un-referenced first. // db.erase (r); // Remove dangling complements and prerequisites. // // Prior to removing a prerequisite/complement we need to make sure it // still exists, which may not be the case due to the dependency cycle. // auto remove = [&c, &db] (const lazy_shared_ptr& rp) { shared_ptr r (db.find (rp.object_id ())); if (r) rep_remove (c, db, r); }; for (const lazy_shared_ptr& cr: r->complements) { // Remove the complement unless it is the root repository (see // rep_fetch() for details). // if (cr.object_id () != "") remove (cr); } for (const lazy_weak_ptr& pr: r->prerequisites) remove (lazy_shared_ptr (pr)); } int rep_remove (const rep_remove_options& o, cli::scanner& args) { tracer trace ("rep_remove"); dir_path c (o.directory ()); l4 ([&]{trace << "configuration: " << c;}); // Check that options and arguments are consistent. // if (o.all ()) { if (args.more ()) fail << "both --all|-a and repository name specified" << info << "run 'bpkg help rep-remove' for more information"; } else if (!args.more ()) fail << "repository name or location argument expected" << info << "run 'bpkg help rep-remove' for more information"; // Build the list of repositories the user wants removed. // vector> repos; database db (open (c, trace)); transaction t (db.begin ()); session s; // Repository dependencies can have cycles. shared_ptr root (db.load ("")); repository::complements_type& ua (root->complements); if (o.all ()) { for (const lazy_shared_ptr& r: ua) repos.push_back (r); } else { while (args.more ()) { // Try to map the argument to a user-added repository. // lazy_shared_ptr r; string a (args.next ()); if (repository_name (a)) { lazy_shared_ptr rp (db, a); // Note: we report repositories we could not find for both cases // below. // if (ua.find (rp) != ua.end ()) r = move (rp); } else { // Note that we can't obtain the canonical name by creating the // repository location object as that would require the repository // type, which is potentially impossible to guess at this stage. So // lets just construct the repository URL and search for it among // user-added repositories. The linear search should be fine as we // don't expect too many of them. // try { repository_url u (a); if (u.empty ()) fail << "empty repository location"; for (const lazy_shared_ptr& rp: ua) { if (rp.load ()->location.url () == u) { r = rp; break; } } } catch (const invalid_argument& e) { fail << "invalid repository location '" << a << "': " << e; } } if (r == nullptr) fail << "repository '" << a << "' does not exist in this " << "configuration"; // Suppress duplicates. // if (find (repos.begin (), repos.end (), r) == repos.end ()) repos.emplace_back (move (r)); } } // Remove the repository references from the root. // // Note that for efficiency we un-reference all the top-level repositories // before starting to delete them. // for (lazy_shared_ptr& r: repos) ua.erase (r); db.update (root); // Remove the dangling repositories from the database, recursively. // for (lazy_shared_ptr& r: repos) { rep_remove (c, db, r.load ()); if (verb) text << "removed " << r.object_id (); } // If we removed all the user-added repositories then no repositories nor // packages should stay in the database. // assert (!ua.empty () || (db.query_value () == 0 && db.query_value () == 0)); t.commit (); return 0; } }