aboutsummaryrefslogtreecommitdiff
path: root/bdep/release.cxx
diff options
context:
space:
mode:
authorBoris Kolpackov <boris@codesynthesis.com>2018-12-15 14:45:24 +0200
committerKaren Arutyunov <karen@codesynthesis.com>2019-01-10 19:43:09 +0300
commitbf3d969ef2dbc615bd528f559920bcf532dda910 (patch)
tree8f965c8a2d883f48fd643bc4cdb3f891ef182b99 /bdep/release.cxx
parent884b16c882863f1eb5144a4bf7d1739bdf99a271 (diff)
Implement bdep-release that manages project's version during release
Diffstat (limited to 'bdep/release.cxx')
-rw-r--r--bdep/release.cxx842
1 files changed, 842 insertions, 0 deletions
diff --git a/bdep/release.cxx b/bdep/release.cxx
new file mode 100644
index 0000000..8c2e496
--- /dev/null
+++ b/bdep/release.cxx
@@ -0,0 +1,842 @@
+// file : bdep/release.cxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2018 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#include <bdep/release.hxx>
+
+#include <libbutl/manifest-types.mxx> // manifest_name_value
+#include <libbutl/manifest-rewriter.mxx>
+
+#include <libbpkg/manifest.hxx>
+
+#include <bdep/git.hxx>
+#include <bdep/project.hxx>
+#include <bdep/diagnostics.hxx>
+
+using namespace std;
+using namespace butl;
+
+namespace bdep
+{
+ // Note: the git_status() function, that we use, requires git 2.11.0 or
+ // higher.
+ //
+ static const semantic_version git_ver {2, 11, 0};
+
+ // Project/package information relevant to what we are doing here.
+ //
+ // Absent optional values mean the corresponding step does not make sense in
+ // this mode or it was suppressed with one of the --no-* options.
+ //
+ // Note that while the release versions can vary in revisions, the open
+ // version is always the same.
+ //
+ namespace
+ {
+ struct package
+ {
+ package_name name;
+ path manifest; // Absolute path.
+
+ // File position of the version value for manifest rewriting.
+ //
+ manifest_name_value version_pos;
+
+ standard_version current_version;
+ optional<standard_version> release_version;
+ };
+
+ struct project
+ {
+ dir_path path;
+ vector<package> packages;
+
+ optional<standard_version> open_version;
+
+ optional<string> tag;
+ bool replace_tag = false;
+ };
+ }
+
+ // The plan_*() functions calculate and set all the new values in the passed
+ // project but don't apply any changes.
+ //
+ static void
+ plan_tag (const cmd_release_options& o, project& prj)
+ {
+ // While there is nothing wrong with having uncommitted changes while
+ // tagging, in our case we may end up with a wrong tag if the version in
+ // the (modified) manifest does not correspond to the latest commit.
+ //
+ // Note that if we are called as part of releasing a new revision, then
+ // the project will have some changes staged (see plan_revision() for
+ // details).
+ //
+ {
+ git_repository_status s (git_status (prj.path));
+
+ if ((s.staged && !o.revision ()) || s.unstaged)
+ fail << "project directory has uncommitted changes" <<
+ info << "run 'git status' for details";
+ }
+
+ // All the versions are the same sans the revision. Note that our version
+ // can be either current (--tag mode) or release (part of the release).
+ //
+ const package& pkg (prj.packages.front ()); // Exemplar package.
+ const standard_version& cv (pkg.release_version
+ ? *pkg.release_version
+ : pkg.current_version);
+
+ if (cv.snapshot ())
+ fail << "current version " << cv << " is a snapshot";
+
+ // Canonical version tag without epoch or revision.
+ //
+ prj.tag = "v" + cv.string_project ();
+
+ // Replace the existing tag only if this is a revision.
+ //
+ prj.replace_tag = (cv.revision != 0);
+ }
+
+ static void
+ plan_open (const cmd_release_options& o, project& prj)
+ {
+ // There could be changes already added to the index but otherwise the
+ // repository should be clean.
+ //
+ {
+ git_repository_status s (git_status (prj.path));
+
+ if (s.unstaged)
+ fail << "project directory has unstaged changes" <<
+ info << "run 'git status' for details";
+ }
+
+ // All the versions are the same sans the revision. Note that our version
+ // can be either current (--open mode) or release (part of the release).
+ //
+ const package& pkg (prj.packages.front ()); // Exemplar package.
+ const standard_version& cv (pkg.release_version
+ ? *pkg.release_version
+ : pkg.current_version);
+
+ // Change the version to the next snapshot. Similar to the release part,
+ // here we have sensible defaults as well as user input:
+ //
+ // 1.2.0 -> 1.3.0-a.0.z
+ // -> 1.2.1-a.0.z --open-patch
+ // -> 2.0.0-a.0.z --open-major
+ //
+ // 1.2.3 -> 1.2.4-a.0.z (assuming bugfix branch)
+ // -> 1.3.0-a.0.z --open-minor
+ // -> 2.0.0-a.0.z --open-major
+ //
+ // 1.2.0-a.1 -> 1.2.0-a.1.z
+ // -> 1.2.0-b.0.z --open-beta
+ //
+ // 1.2.0-b.1 -> 1.2.0-b.1.z
+ //
+ // Note that there is no --open-alpha since that's either the default or
+ // would be invalid (jumping backwards).
+ //
+ auto& ov (prj.open_version);
+
+ auto make_snapshot = [&cv] (uint16_t major,
+ uint16_t minor,
+ uint16_t patch,
+ uint16_t pre_release = 0 /* a.0 */)
+ {
+ return standard_version (cv.epoch,
+ major,
+ minor,
+ patch,
+ pre_release,
+ standard_version::latest_sn,
+ "" /* snapshot_id */);
+ };
+
+ if (cv.snapshot ())
+ {
+ // The cv variable refers the current version as the release version
+ // cannot be a snapshot.
+ //
+ assert (!pkg.release_version);
+
+ fail << "current version " << cv << " is a snapshot";
+ }
+ else if (cv.alpha ())
+ {
+ if (const char* n = (o.open_patch () ? "--open-patch" :
+ o.open_minor () ? "--open-minor" :
+ o.open_major () ? "--open-major" : nullptr))
+ fail << n << " specified for alpha current version " << cv;
+
+ ov = make_snapshot (cv.major (),
+ cv.minor (),
+ cv.patch (),
+ o.open_beta () ? 500 : cv.pre_release ());
+ }
+ else if (cv.beta ())
+ {
+ if (const char* n = (o.open_patch () ? "--open-patch" :
+ o.open_minor () ? "--open-minor" :
+ o.open_major () ? "--open-major" : nullptr))
+ fail << n << " specified for beta current version " << cv;
+
+ ov = make_snapshot (cv.major (),
+ cv.minor (),
+ cv.patch (),
+ cv.pre_release () + 500);
+ }
+ else
+ {
+ if (const char* n = (o.open_beta () ? "--open-beta" : nullptr))
+ fail << n << " specified for final current version " << cv;
+
+ uint16_t mj (cv.major ());
+ uint16_t mi (cv.minor ());
+ uint16_t pa (cv.patch ());
+
+ if (o.open_major ()) {mj++; mi = pa = 0;}
+ else if (o.open_minor ()) { mi++; pa = 0;}
+ else if (o.open_patch ()) { pa++; }
+ else if (pa == 0) { mi++; } // Default open minor.
+ else { pa++; } // Default open patch.
+
+ ov = make_snapshot (mj, mi, pa);
+ }
+ }
+
+ static void
+ plan_version (const cmd_release_options& o, project& prj)
+ {
+ // There could be changes already added to the index but otherwise the
+ // repository should be clean.
+ //
+ {
+ git_repository_status s (git_status (prj.path));
+
+ if (s.unstaged)
+ fail << "project directory has unstaged changes" <<
+ info << "run 'git status' for details";
+ }
+
+ // All the current versions are the same sans the revision.
+ //
+ const standard_version& cv (prj.packages.front ().current_version);
+
+ // Change the release version to the next (pre-)release.
+ //
+ standard_version rv;
+ if (cv.snapshot ())
+ {
+ // To recap, a snapshot is expressed as a version *after* the previous
+ // pre-release or after an imaginary a.0, if it was the final release.
+ // Which means we cannot know for certain what the next (pre-)release
+ // version should be. However, the final release seems like a reasonable
+ // default. Consider:
+ //
+ // 1.2.3-a.0.z -> 1.2.3 (without pre-release)
+ // 1.2.3-a.2.z -> 1.2.3 (a.2 is last pre-release)
+ // 1.2.3-b.1.z -> 1.2.3 (b.1 is last pre-release)
+ //
+ // And the alternatives would be to release an alpha, a beta, or to jump
+ // to the next minor (if we opened with a patch increment) or major (if
+ // we opened with a minor or patch increment) version.
+ //
+ // Note that there is no --patch since we cannot jump to patch.
+ //
+ uint16_t mj (cv.major ());
+ uint16_t mi (cv.minor ());
+ uint16_t pa (cv.patch ());
+ uint16_t pr (cv.pre_release ());
+
+ if (o.major ()) {mj++; mi = pa = pr = 0;}
+ else if (o.minor ()) { mi++; pa = pr = 0;}
+ else if (o.beta ())
+ {
+ pr = (cv.beta () ? pr : 0) + 1 + 500; // Next/first beta.
+ }
+ else if (o.alpha ())
+ {
+ if (cv.beta ())
+ fail << "--alpha specified for beta current version " << cv;
+
+ pr++; // Next alpha.
+ }
+ else
+ pr = 0; // Final.
+
+ rv = standard_version (cv.epoch, mj, mi, pa, pr);
+ }
+ else if (cv.alpha () || cv.beta ())
+ {
+ // Releasing from alpha/beta. For example, alpha/beta becomes the final
+ // release without going through a snapshot.
+ //
+ if (const char* n = (o.minor () ? "--minor" :
+ o.major () ? "--major" : nullptr))
+ fail << n << " specified for " << (cv.beta () ? "beta" : "alpha")
+ << " current version " << cv;
+
+ uint16_t pr (cv.pre_release ());
+
+ if (o.beta ())
+ {
+ pr = (cv.beta () ? pr : 0) + 1 + 500; // Next/first beta.
+ }
+ else if (o.alpha ())
+ {
+ if (cv.beta ())
+ fail << "--alpha specified for beta current version " << cv;
+
+ pr++; // Next alpha.
+ }
+ else
+ pr = 0; // Final.
+
+ rv = standard_version (cv.epoch,
+ cv.major (),
+ cv.minor (),
+ cv.patch (),
+ pr);
+ }
+ else
+ fail << "current version " << cv << " is not a snapshot or pre-release";
+
+ // Update the version in each package.
+ //
+ for (package& p: prj.packages)
+ p.release_version = rv;
+
+ // If we are not committing, then the rest doesn't apply.
+ //
+ if (o.no_commit ())
+ return;
+
+ if (!o.no_tag ())
+ plan_tag (o, prj);
+
+ if (!o.no_open ())
+ plan_open (o, prj);
+ }
+
+ static void
+ plan_revision (const cmd_release_options& o, project& prj)
+ {
+ // There must be changes already added to the index but otherwise the
+ // repository should be clean.
+ //
+ {
+ git_repository_status s (git_status (prj.path));
+
+ if (s.unstaged)
+ fail << "project directory has unstaged changes" <<
+ info << "run 'git status' for details";
+
+ if (!s.staged)
+ fail << "project directory has no staged changes" <<
+ info << "revision increment must be committed together with "
+ << "associated changes";
+ }
+
+ // All the current versions are the same sans the revision.
+ //
+ const standard_version& cv (prj.packages.front ().current_version);
+
+ if (cv.snapshot ())
+ fail << "current version " << cv << " is a snapshot";
+
+ // Increment the revision in each package manifest.
+ //
+ for (package& p: prj.packages)
+ {
+ p.release_version = p.current_version;
+ p.release_version->revision++;
+ }
+
+ // If we are not committing, then the rest doesn't apply.
+ //
+ if (o.no_commit ())
+ return;
+
+ if (!o.no_tag ())
+ plan_tag (o, prj);
+ }
+
+ int
+ cmd_release (const cmd_release_options& o, cli::scanner&)
+ {
+ // Detect options incompatibility going through the groups of mutually
+ // exclusive options. Also make sure that options make sense for the
+ // current mode (releasing, revising, etc.) by pre-setting an incompatible
+ // mode option (mopt) as the first group members.
+ //
+ {
+ // Points to the group option, if any is specified and NULL otherwise.
+ //
+ const char* gopt (nullptr);
+
+ // If an option is specified on the command line, then check that no
+ // group option have been specified yet and set it, if that's the case,
+ // or fail otherwise.
+ //
+ auto verify = [&gopt] (const char* opt, bool specified)
+ {
+ if (specified)
+ {
+ if (gopt == nullptr)
+ gopt = opt;
+ else
+ fail << "both " << gopt << " and " << opt << " specified";
+ }
+ };
+
+ // Check the mode options.
+ //
+ verify ("--revision", o.revision ());
+ verify ("--open", o.open ());
+ verify ("--tag", o.tag ());
+
+ // The current mode option (--revision, --open, --tag, or NULL for the
+ // releasing mode).
+ //
+ // Prior to verifying an option group we will be setting gopt to the
+ // current mode option if the group options are meaningless for this
+ // mode or to NULL otherwise.
+ //
+ const char* mopt (gopt);
+
+ // The following (mutually exclusive) options are only meaningful for
+ // the releasing mode.
+ //
+ gopt = mopt;
+ verify ("--alpha", o.alpha ());
+ verify ("--beta", o.beta ());
+ verify ("--minor", o.minor ());
+ verify ("--major", o.major ());
+
+ // The following option is only meaningful for the releasing mode.
+ //
+ gopt = mopt;
+ verify ("--no-open", o.no_open ());
+
+ // The following option is only meaningful for the releasing and
+ // revising modes.
+ //
+ gopt = o.revision () ? nullptr : mopt;
+ verify ("--no-tag", o.no_tag ());
+
+ // The following option is only meaningful for the releasing, revising,
+ // and opening modes.
+ //
+ gopt = o.revision () || o.open () ? nullptr : mopt;
+ verify ("--no-commit", o.no_commit ());
+
+ // The following (mutually exclusive) options are only meaningful for
+ // the releasing and opening modes.
+ //
+ gopt = o.open () ? nullptr : mopt;
+ verify ("--open-beta", o.open_beta ());
+ verify ("--open-patch", o.open_patch ());
+ verify ("--open-minor", o.open_minor ());
+ verify ("--open-major", o.open_major ());
+ verify ("--no-open", o.no_open ()); // Releasing only (see above).
+
+ // There is no sense to push without committing the version change first.
+ //
+ gopt = nullptr;
+ verify ("--push", o.push ()); // Meaningful for all modes.
+ verify ("--no-commit", o.no_commit ()); // See above for modes.
+ }
+
+ // Fully parse package manifest verify it is valid and returning the
+ // position of the version value. In a sense we are publishing (by
+ // tagging) to a version control-based repository and it makes sense to
+ // ensure the repository will not be broken, similar to how we do it in
+ // publish.
+ //
+ auto parse_manifest = [] (const path& f)
+ {
+ manifest_name_value r;
+
+ auto m (bdep::parse_manifest<bpkg::package_manifest> (
+ f,
+ "package",
+ false /* ignore_unknown */,
+ [&r] (manifest_name_value& nv)
+ {
+ if (nv.name == "version")
+ r = nv;
+
+ return true;
+ }));
+
+ // Validate the *-file manifest values expansion.
+ //
+ m.load_files ([&f] (const string& n, const path& p)
+ {
+ path vf (f.directory () / p);
+
+ try
+ {
+ ifdstream is (vf);
+ string s (is.read_text ());
+
+ if (s.empty ())
+ fail << n << " manifest value in " << f << " references empty "
+ << "file " << vf;
+
+ return s;
+ }
+ catch (const io_error& e)
+ {
+ fail << "unable to read " << vf << " referenced by " << n
+ << " manifest value in " << f << endf;
+ }
+ });
+
+ assert (!r.empty ());
+ r.value = m.version.string (); // For good measure.
+ return r;
+ };
+
+ // Collect project/package information.
+ //
+ project prj;
+ {
+ // We publish all the packages in the project. We could have required a
+ // configuration and verified that they are all initialized, similar to
+ // publish. But seeing that we don't need the configuration (unlike
+ // publish), this feels like an unnecessary complication. We also don't
+ // pre-sync for the same reasons.
+ //
+ // Seeing that we are tagging the entire repository, force the
+ // collection of all the packages even if the current working directory
+ // is a package. But allow explicit package directory specification as
+ // the "I know what I am doing" mode (e.g., to sidestep the same version
+ // restriction).
+ //
+ package_locations pls;
+
+ if (o.directory_specified ())
+ {
+ project_packages pp (
+ find_project_packages (o.directory (),
+ false /* ignore_packages */,
+ true /* load_packages */));
+ prj.path = move (pp.project);
+ pls = move (pp.packages);
+ }
+ else
+ {
+ prj.path = find_project (o.directory ());
+ pls = load_packages (prj.path);
+ }
+
+ for (package_location& pl: pls)
+ {
+ // Parse each manifest extracting name, version, position, etc.
+ //
+ path f (prj.path / pl.path / manifest_file);
+ manifest_name_value vv (parse_manifest (f));
+
+ package_name n (move (pl.name));
+ standard_version v;
+ try
+ {
+ // Allow stubs only in the --revision mode.
+ //
+ standard_version::flags f (standard_version::none);
+
+ if (o.revision ())
+ f |= standard_version::allow_stub;
+
+ v = standard_version (vv.value, f);
+ }
+ catch (const invalid_argument&)
+ {
+ fail << "current package " << n << " version " << vv.value
+ << " is not standard";
+ }
+
+ prj.packages.push_back (
+ package {move (n),
+ move (f),
+ move (vv),
+ move (v),
+ nullopt /* release_version */});
+ }
+ }
+
+ // Verify all the packages have the same version. This is the only
+ // arrangement we currently (and probably ever) support. The immediate
+ // problem with supporting different versions (besides the extra
+ // complexity, of course) is tagging. But since our tags don't include
+ // revisions, we do allow variations in that.
+ //
+ // While at it, notice if we will end up with different revisions which
+ // we use below when forming the commit message.
+ //
+ bool multi_rev (false);
+ {
+ const package& f (prj.packages.front ());
+
+ for (const package& p: prj.packages)
+ {
+ const auto& fv (f.current_version);
+ const auto& pv (p.current_version);
+
+ if (fv.compare (pv, true /* ignore_revision */) != 0)
+ {
+ fail << "different current package versions" <<
+ info << "package " << f.name << " version " << fv <<
+ info << "package " << p.name << " version " << pv;
+ }
+
+ multi_rev = multi_rev || fv.revision != pv.revision;
+ }
+
+ multi_rev = multi_rev && o.revision ();
+ }
+
+ // Plan the changes.
+ //
+ const char* mode;
+ if (o.revision ()) {plan_revision (o, prj); mode = "revising";}
+ else if (o.open ()) {plan_open (o, prj); mode = "opening";}
+ else if (o.tag ()) {plan_tag (o, prj); mode = "tagging";}
+ else {plan_version (o, prj); mode = "releasing";}
+
+ const package& pkg (prj.packages.front ()); // Exemplar package.
+
+ bool commit (!o.no_commit () && (pkg.release_version || prj.open_version));
+ bool push (o.push ());
+
+ // Print the plan and ask for confirmation.
+ //
+ if (!o.yes ())
+ {
+ diag_record dr (text);
+
+ dr << mode << ":" << '\n';
+
+ for (const package& p: prj.packages)
+ {
+ dr << " package: " << p.name << '\n'
+ << " current: " << p.current_version << '\n';
+
+ if (p.release_version)
+ dr << " release: " << *p.release_version << '\n';
+
+ if (prj.open_version)
+ dr << " open: " << *prj.open_version << '\n';
+
+ // If printing multiple packages, separate them with a blank line.
+ //
+ if (prj.packages.size () > 1)
+ dr << '\n';
+ }
+
+ if (!o.tag ())
+ dr << " commit: " << (commit ? "yes" : "no") << '\n';
+
+ dr << " tag: " << (prj.tag ? prj.tag->c_str () : "no") << '\n'
+ << " push: " << (push ? "yes" : "no");
+
+ dr.flush ();
+
+ if (!yn_prompt ("continue? [y/n]"))
+ return 1;
+ }
+
+ // Stage and commit the project changes.
+ //
+ auto commit_project = [&prj] (const string& msg)
+ {
+ // We shouldn't have any untracked files or unstaged changes other than
+ // our modifications, so -a is good enough.
+ //
+ run_git (git_ver,
+ prj.path,
+ "commit",
+ verb < 2 ? "-q" : verb > 2 ? "-v" : nullptr,
+ "-a",
+ "-m", msg);
+ };
+
+ // Release.
+ //
+ if (pkg.release_version)
+ {
+ // Rewrite each package manifest.
+ //
+ for (package& p: prj.packages)
+ try
+ {
+ manifest_name_value& vv (p.version_pos);
+
+ // Rewrite the version.
+ //
+ {
+ manifest_rewriter rw (p.manifest);
+ vv.value = p.release_version->string ();
+ rw.replace (vv);
+ }
+
+ // If we also need to open the next development cycle, update the
+ // version position for the subsequent manifest rewrite.
+ //
+ if (prj.open_version)
+ vv = parse_manifest (p.manifest);
+ }
+ // The IO failure is unlikely to happen (as we have already read the
+ // manifests) but still possible (write permission is denied, device is
+ // full, etc.). In this case we may potentially leave the project in an
+ // inconsistent state as some of the package manifests could have
+ // already been rewritten. As a result, the subsequent bdep-release may
+ // fail due to unstaged changes.
+ //
+ catch (const io_error& e)
+ {
+ fail << "unable to read/write " << p.manifest << ": " << e <<
+ info << "run 'git -C " << prj.path << " checkout -- ./' to revert "
+ << "any changes and try again";
+ }
+
+ // If not committing, then we are done.
+ //
+ // In this case it would have been nice to pre-populate the commit
+ // message but there doesn't seem to be a way to do that. In particular,
+ // writing to .git/COMMIT_EDITMSG does not work (existing content is
+ // discarded).
+ //
+ if (!commit)
+ return 0;
+
+ // Commit the manifest rewrites.
+ //
+ // If we are releasing multiple revisions, then list every package in
+ // the form:
+ //
+ // Release versions hello/0.1.0+1, libhello/0.1.0+2
+ //
+ string m;
+ if (multi_rev)
+ {
+ for (const package& p: prj.packages)
+ {
+ m += m.empty () ? "Release versions " : ", ";
+ m += p.name.string ();
+ m += '/';
+ m += p.release_version->string ();
+ }
+ }
+ else
+ m = "Release version " + pkg.release_version->string ();
+
+ commit_project (m);
+ }
+
+ // Tag.
+ //
+ if (prj.tag)
+ {
+ // Note that our version can be either current (--tag mode) or release
+ // (part of the release).
+ //
+ const standard_version& cv (pkg.release_version
+ ? *pkg.release_version
+ : pkg.current_version);
+
+ // Note that the tag may exist both locally and remotely or only in one
+ // of the places. The remote case is handled by push.
+ //
+ // The git-tag command with -f will replace the existing tag but may
+ // print to stdout (which we redirect to stderr) something like:
+ //
+ // Updated tag 'v0.1.0' (was 8f689ec)
+ //
+ run_git (git_ver,
+ prj.path,
+ "tag",
+ prj.replace_tag ? "-f" : nullptr,
+ "-a", *prj.tag,
+ "-m", "Tag version " + cv.string ());
+ }
+
+ // Open.
+ //
+ if (prj.open_version)
+ {
+ string ov (prj.open_version->string ());
+
+ // Rewrite each package manifest (similar code to above).
+ //
+ for (package& p: prj.packages)
+ try
+ {
+ manifest_rewriter rw (p.manifest);
+ p.version_pos.value = ov;
+ rw.replace (p.version_pos);
+ }
+ catch (const io_error& e)
+ {
+ // If we are releasing, then the release/revision version have already
+ // been written to the manifests and the changes have been committed.
+ // Thus, the user should re-try with the --open option in this case.
+ //
+ fail << "unable to read/write " << p.manifest << ": " << e <<
+ info << "run 'git -C " << prj.path << " checkout -- ./' to revert "
+ << "any changes and try again" << (pkg.release_version
+ ? " with --open"
+ : "");
+ }
+
+ if (!commit)
+ return 0;
+
+ // Commit the manifest rewrites.
+ //
+ commit_project ("Change version to " + ov);
+ }
+
+ if (push)
+ {
+ // It would have been nice to push commits and tags using just the
+ // --follow-tags option. However, this doesn't work if we need to
+ // replace the tag in the remote repository. Thus, we specify the
+ // repository and refspecs explicitly.
+ //
+ string tagspec;
+
+ if (prj.tag)
+ {
+ // Force update of the remote tag, if required.
+ //
+ if (prj.replace_tag)
+ tagspec += '+';
+
+ tagspec += "refs/tags/";
+ tagspec += *prj.tag;
+ }
+
+ // Note that we suppress the (too detailed) push command output if
+ // the verbosity level is 1. However, we still want to see the
+ // progress in this case.
+ //
+ run_git (git_ver,
+ prj.path,
+ "push",
+ verb < 2 ? "-q" : verb > 3 ? "-v" : nullptr,
+ verb == 1 ? "--progress" : nullptr,
+ "origin",
+ "HEAD",
+ !tagspec.empty () ? tagspec.c_str () : nullptr);
+ }
+
+ return 0;
+ }
+}