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/ci.cxx | 295 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 bdep/ci.cxx (limited to 'bdep/ci.cxx') 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; + } +} -- cgit v1.1