aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--bdep/bdep.cli5
-rw-r--r--bdep/bdep.cxx2
-rw-r--r--bdep/buildfile2
-rw-r--r--bdep/git.hxx2
-rw-r--r--bdep/git.txx9
-rw-r--r--bdep/project.cxx6
-rw-r--r--bdep/project.hxx17
-rw-r--r--bdep/release.cli166
-rw-r--r--bdep/release.cxx842
-rw-r--r--bdep/release.hxx19
-rw-r--r--bdep/sync.cxx17
-rw-r--r--bdep/utility.hxx25
-rw-r--r--bdep/utility.txx30
-rwxr-xr-xdoc/cli.sh4
-rw-r--r--tests/common.testscript1
-rw-r--r--tests/release.testscript634
16 files changed, 1749 insertions, 32 deletions
diff --git a/bdep/bdep.cli b/bdep/bdep.cli
index 2b8b278..c743686 100644
--- a/bdep/bdep.cli
+++ b/bdep/bdep.cli
@@ -448,6 +448,11 @@ namespace bdep
"\l{bdep-ci(1)} \- submit project test request to CI server"
}
+ bool release
+ {
+ "\l{bdep-release(1)} \- manage project's version during release"
+ }
+
bool publish
{
"\l{bdep-publish(1)} \- publish project to archive repository"
diff --git a/bdep/bdep.cxx b/bdep/bdep.cxx
index 8fbcd71..e6f850e 100644
--- a/bdep/bdep.cxx
+++ b/bdep/bdep.cxx
@@ -27,6 +27,7 @@
#include <bdep/fetch.hxx>
#include <bdep/status.hxx>
#include <bdep/ci.hxx>
+#include <bdep/release.hxx>
#include <bdep/publish.hxx>
#include <bdep/deinit.hxx>
#include <bdep/config.hxx>
@@ -298,6 +299,7 @@ try
COMMAND_IMPL (fetch, fetch, "fetch", true);
COMMAND_IMPL (status, status, "status", true);
COMMAND_IMPL (ci, ci, "ci", true);
+ COMMAND_IMPL (release, release, "release", 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 f77282a..274700e 100644
--- a/bdep/buildfile
+++ b/bdep/buildfile
@@ -28,6 +28,7 @@ sync-options \
fetch-options \
status-options \
ci-options \
+release-options \
publish-options \
deinit-options \
config-options \
@@ -79,6 +80,7 @@ if $cli.configured
cli.cxx{fetch-options}: cli{fetch}
cli.cxx{status-options}: cli{status}
cli.cxx{ci-options}: cli{ci}
+ cli.cxx{release-options}: cli{release}
cli.cxx{publish-options}: cli{publish}
cli.cxx{deinit-options}: cli{deinit}
cli.cxx{config-options}: cli{config}
diff --git a/bdep/git.hxx b/bdep/git.hxx
index 91a9fd9..ee8847c 100644
--- a/bdep/git.hxx
+++ b/bdep/git.hxx
@@ -91,6 +91,8 @@ namespace bdep
bool behind = false; // Local branch is behind of upstream.
};
+ // Note: requires git 2.11.0 or higher.
+ //
git_repository_status
git_status (const dir_path& repo);
}
diff --git a/bdep/git.txx b/bdep/git.txx
index 4abae3c..c4202de 100644
--- a/bdep/git.txx
+++ b/bdep/git.txx
@@ -8,9 +8,16 @@ namespace bdep
void
run_git (const semantic_version& min_ver, const dir_path& repo, A&&... args)
{
+ // We don't expect git to print anything to stdout, as the caller would use
+ // start_git() and pipe otherwise. Thus, let's redirect stdout to stderr
+ // for good measure, as git is known to print some informational messages
+ // to stdout.
+ //
process pr (start_git (min_ver,
repo,
- 0 /* stdin */, 1 /* stdout */, 2 /* stderr */,
+ 0 /* stdin */,
+ 2 /* stdout */,
+ 2 /* stderr */,
forward<A> (args)...));
finish_git (pr);
diff --git a/bdep/project.cxx b/bdep/project.cxx
index 8178b01..f6ca5bf 100644
--- a/bdep/project.cxx
+++ b/bdep/project.cxx
@@ -263,15 +263,15 @@ namespace bdep
}
project_packages
- find_project_packages (const project_options& po,
+ find_project_packages (const dir_paths& dirs,
bool ignore_packages,
bool load_packages)
{
project_packages r;
- if (po.directory_specified ())
+ if (!dirs.empty ())
{
- for (const dir_path& d: po.directory ())
+ for (const dir_path& d: dirs)
{
project_package p (find_project_package (d));
diff --git a/bdep/project.hxx b/bdep/project.hxx
index e508374..c252ead 100644
--- a/bdep/project.hxx
+++ b/bdep/project.hxx
@@ -205,11 +205,26 @@ namespace bdep
package_locations packages;
};
+ // Search project packages in the specified directories or the current
+ // directory if none were specified.
+ //
project_packages
- find_project_packages (const project_options&,
+ find_project_packages (const dir_paths&,
bool ignore_packages,
bool load_packages = true);
+ inline project_packages
+ find_project_packages (const project_options& po, bool ip, bool lp = true)
+ {
+ return find_project_packages (po.directory (), ip, lp);
+ }
+
+ inline dir_path
+ find_project (const dir_paths& dirs)
+ {
+ return find_project_packages (dirs, true /* ignore_packages */).project;
+ }
+
inline dir_path
find_project (const project_options& o)
{
diff --git a/bdep/release.cli b/bdep/release.cli
new file mode 100644
index 0000000..10c4b58
--- /dev/null
+++ b/bdep/release.cli
@@ -0,0 +1,166 @@
+// file : bdep/release.cli
+// copyright : Copyright (c) 2014-2018 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+include <bdep/common.cli>;
+
+"\section=1"
+"\name=bdep-release"
+"\summary=manage project's version during release"
+
+namespace bdep
+{
+ {
+ "<options>
+ <prj-spec> <prj-dir>
+ <pkg-spec> <pkg-dir>",
+
+ "\h|SYNOPSIS|
+
+ \c{\b{bdep release} [<options>] [<prj-spec>]}
+
+ \c{<prj-spec> = \b{--directory}|\b{-d} <prj-dir> | <pkg-spec>\n
+ <pkg-spec> = (\b{--directory}|\b{-d} <pkg-dir>)...}
+
+ \h|DESCRIPTION|
+
+ The \cb{release} command manages the project's version during the
+ release. Specifically, it first changes the snapshot version to the
+ corresponding release version in each project package's \cb{manifest}
+ file, commits these changes (unless \cb{--no-commit} is specified), tags
+ this commit (unless \cb{--no-tag} is specified), and, if \cb{--push} is
+ specified, pushes the changes to the remote. Unless \cb{--no-open} is
+ specified, the \cb{release} command then opens the next development cycle
+ by changing the version to a snapshot, committing these changes (unless
+ \cb{--no-commit} is specified), and, if \cb{--push} is specified, pushing
+ them to the remote. Note that committing, tagging, and pushing is
+ currently only supported for \cb{git(1)} project repositories.
+
+ The \cb{release} command can also be used to release a new package
+ revision by passing the \cb{--revision} option. In this mode \cb{release}
+ increments the current version's revision component in each project
+ package's \cb{manifest} file, commits these changes (unless
+ \cb{--no-commit} is specified), tags this commit (unless \cb{--no-tag} is
+ specified), and, if \cb{--push} is specified, pushes the changes to the
+ remote. Note that in this case the project's repository index is expected
+ to already contain other changes since for a revision all the associated
+ changes, including to version, must belong to a single commit. In this
+ mode \cb{release} will also silently replace an existing tag for the same
+ version.
+
+ The \cb{release} command also has a number of \i{continue modes} that
+ allow the completion of steps that were previously suppressed with the
+ \cb{--no-*} options in the above main modes. These are \cb{--tag} which
+ tags the release commit and, if \cb{--push} is specified, pushes it to
+ the remote as well as \cb{--open} which performs the opening of the next
+ development cycle as described above.
+
+ Normally, \cb{release} operates on all the packages in a project. If no
+ project directory is specified, then the current working directory is
+ assumed and all the packages are released, even if the current directory
+ is a package directory. If, however, one or more package directories are
+ specified explicitly with \c{\b{--directory}|\b{-d}}, then \cb{release}
+ assumes you know what you are doing and only releases these packages.
+ All the packages being released must have the same version but may have
+ different revisions.
+ "
+ }
+
+ class cmd_release_options: common_options
+ {
+ "\h|RELEASE OPTIONS|"
+
+ bool --revision
+ {
+ "Release a new package revision instead of a new version."
+ }
+
+ bool --no-commit
+ {
+ "Don't commit the changes. Implies \cb{--no-tag} and, in the version
+ release mode, \cb{--no-open}."
+ }
+
+ bool --no-tag
+ {
+ "Don't tag the release commit. Tagging can be performed later using the
+ \cb{--tag} mode option."
+ }
+
+ bool --tag
+ {
+ "Tag the already released version instead of releasing a new one."
+ }
+
+ bool --push
+ {
+ "Push the committed changes and tags to the remote."
+ }
+
+ bool --no-open
+ {
+ "Don't open the next development cycle. Opening can be performed later
+ using the \cb{--open} mode option."
+ }
+
+ bool --open
+ {
+ "Open the next development cycle instead of releasing a new version."
+ }
+
+ bool --alpha
+ {
+ "Release an alpha instead of the final version."
+ }
+
+ bool --beta
+ {
+ "Release a beta version instead of the final version."
+ }
+
+ bool --minor
+ {
+ "Release the next minor version instead of the current patch."
+ }
+
+ bool --major
+ {
+ "Release the next major version instead of the current minor or patch."
+ }
+
+ bool --open-beta
+ {
+ "Open the development cycle with the next beta version."
+ }
+
+ bool --open-patch
+ {
+ "Open the development cycle with the next patch version. This is the
+ default if the current patch version is not \c{0} (bugfix release
+ series)."
+ }
+
+ bool --open-minor
+ {
+ "Open the development cycle with the next minor version. This is the
+ default if the current patch version is \c{0} (feature release series)."
+ }
+
+ bool --open-major
+ {
+ "Open the development cycle with the next major version."
+ }
+
+ bool --yes|-y
+ {
+ "Don't prompt for confirmation before releasing."
+ }
+
+ dir_paths --directory|-d
+ {
+ "<dir>",
+ "Assume project/package is in the specified directory rather than in the
+ current working directory."
+ }
+ };
+}
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;
+ }
+}
diff --git a/bdep/release.hxx b/bdep/release.hxx
new file mode 100644
index 0000000..305f8c2
--- /dev/null
+++ b/bdep/release.hxx
@@ -0,0 +1,19 @@
+// file : bdep/release.hxx -*- C++ -*-
+// copyright : Copyright (c) 2014-2018 Code Synthesis Ltd
+// license : MIT; see accompanying LICENSE file
+
+#ifndef BDEP_RELEASE_HXX
+#define BDEP_RELEASE_HXX
+
+#include <bdep/types.hxx>
+#include <bdep/utility.hxx>
+
+#include <bdep/release-options.hxx>
+
+namespace bdep
+{
+ int
+ cmd_release (const cmd_release_options&, cli::scanner& args);
+}
+
+#endif // BDEP_RELEASE_HXX
diff --git a/bdep/sync.cxx b/bdep/sync.cxx
index b9a83a0..dfc9f9c 100644
--- a/bdep/sync.cxx
+++ b/bdep/sync.cxx
@@ -115,16 +115,19 @@ namespace bdep
// Project to be synchronized.
//
- struct project
+ namespace
{
- dir_path path;
- shared_ptr<configuration> config;
+ struct project
+ {
+ dir_path path;
+ shared_ptr<configuration> config;
- bool implicit;
- bool fetch;
- };
+ bool implicit;
+ bool fetch;
+ };
- using projects = small_vector<project, 1>;
+ using projects = small_vector<project, 1>;
+ }
// Append the list of additional (to origin, if not empty) projects that are
// using this configuration.
diff --git a/bdep/utility.hxx b/bdep/utility.hxx
index 57764fa..1f45ded 100644
--- a/bdep/utility.hxx
+++ b/bdep/utility.hxx
@@ -19,6 +19,9 @@
#include <libbutl/fdstream.mxx>
#include <libbutl/filesystem.mxx>
+#include <libbutl/manifest-parser.mxx> // manifest_parser::filter_function
+#include <libbutl/manifest-serializer.mxx> // manifest_serializer::filter_function
+
#include <bdep/types.hxx>
#include <bdep/version.hxx>
#include <bdep/common-options.hxx>
@@ -208,25 +211,33 @@ namespace bdep
T
parse_manifest (const path&,
const char* what,
- bool ignore_unknown = false);
+ bool ignore_unknown = false,
+ function<butl::manifest_parser::filter_function> = {});
template <typename T>
T
parse_manifest (istream&,
const string& name,
const char* what,
- bool ignore_unknown = false);
+ bool ignore_unknown = false,
+ function<butl::manifest_parser::filter_function> = {});
template <typename T>
void
- serialize_manifest (const T&, const path&, const char* what);
+ serialize_manifest (
+ const T&,
+ const path&,
+ const char* what,
+ function<butl::manifest_serializer::filter_function> = {});
template <typename T>
void
- serialize_manifest (const T&,
- ostream&,
- const string& name,
- const char* what);
+ serialize_manifest (
+ const T&,
+ ostream&,
+ const string& name,
+ const char* what,
+ function<butl::manifest_serializer::filter_function> = {});
// CLI (sub)command parsing helper.
//
diff --git a/bdep/utility.txx b/bdep/utility.txx
index 9926e12..cab2dd8 100644
--- a/bdep/utility.txx
+++ b/bdep/utility.txx
@@ -4,9 +4,6 @@
#include <iostream> // cin
-#include <libbutl/manifest-parser.mxx>
-#include <libbutl/manifest-serializer.mxx>
-
#include <bdep/diagnostics.hxx>
namespace bdep
@@ -236,7 +233,10 @@ namespace bdep
//
template <typename T>
T
- parse_manifest (const path& f, const char* what, bool iu)
+ parse_manifest (const path& f,
+ const char* what,
+ bool iu,
+ function<butl::manifest_parser::filter_function> ff)
{
using namespace butl;
@@ -249,7 +249,7 @@ namespace bdep
fail << what << " manifest file " << f << " does not exist";
ifdstream ifs (f);
- return parse_manifest<T> (ifs, f.string (), what, iu);
+ return parse_manifest<T> (ifs, f.string (), what, iu, move (ff));
}
catch (const system_error& e) // EACCES, etc.
{
@@ -260,13 +260,17 @@ namespace bdep
template <typename T>
T
- parse_manifest (istream& is, const string& name, const char* what, bool iu)
+ parse_manifest (istream& is,
+ const string& name,
+ const char* what,
+ bool iu,
+ function<butl::manifest_parser::filter_function> ff)
{
using namespace butl;
try
{
- manifest_parser p (is, name);
+ manifest_parser p (is, name, move (ff));
return T (p, iu);
}
catch (const manifest_parsing& e)
@@ -283,7 +287,10 @@ namespace bdep
template <typename T>
void
- serialize_manifest (const T& m, const path& f, const char* what)
+ serialize_manifest (const T& m,
+ const path& f,
+ const char* what,
+ function<butl::manifest_serializer::filter_function> ff)
{
using namespace std;
using namespace butl;
@@ -293,7 +300,7 @@ namespace bdep
ofdstream ofs (f, ios::binary);
auto_rmfile arm (f); // Try to remove on failure ignoring errors.
- serialize_manifest (m, ofs, f.string (), what);
+ serialize_manifest (m, ofs, f.string (), what, move (ff));
ofs.close ();
arm.cancel ();
@@ -309,13 +316,14 @@ namespace bdep
serialize_manifest (const T& m,
ostream& os,
const string& name,
- const char* what)
+ const char* what,
+ function<butl::manifest_serializer::filter_function> ff)
{
using namespace butl;
try
{
- manifest_serializer s (os, name);
+ manifest_serializer s (os, name, move (ff));
m.serialize (s);
return;
}
diff --git a/doc/cli.sh b/doc/cli.sh
index afce007..534a57e 100755
--- a/doc/cli.sh
+++ b/doc/cli.sh
@@ -62,8 +62,8 @@ 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 ci publish deinit config test update \
-clean projects-configs"
+pages="new help init sync fetch status ci release publish deinit config test \
+update clean projects-configs"
for p in $pages; do
compile $p $o
diff --git a/tests/common.testscript b/tests/common.testscript
index 400ae48..5397fe1 100644
--- a/tests/common.testscript
+++ b/tests/common.testscript
@@ -53,6 +53,7 @@ sync = $* sync
update = $* update
config = $* config
publish = $* publish
+release = $* release
# All testscripts are named after bdep commands, for example sync.testscript.
# So the testscript scope id is a name of the command being tested.
diff --git a/tests/release.testscript b/tests/release.testscript
new file mode 100644
index 0000000..14d315b
--- /dev/null
+++ b/tests/release.testscript
@@ -0,0 +1,634 @@
+# file : tests/release.testscript
+# copyright : Copyright (c) 2014-2018 Code Synthesis Ltd
+# license : MIT; see accompanying LICENSE file
+
+.include common.testscript project.testscript
+
+# bdep-release requirements for the minimum supported git version are higher
+# then the default 2.1.0 (see bdep/release.cxx for details).
+#
++if! ($git_version_major > 2 || \
+ $git_version_major == 2 && $git_version_minor >= 11)
+ exit
+end
+
+g = git 2>! >&2
+
+# Create the remote repository.
+#
++mkdir --no-cleanup prj.git
++$g -C prj.git init --bare &prj.git/***
+
+clone_rep = cp --no-cleanup -p -r ../prj.git ./ &prj.git/***
+clone_root_rep = cp --no-cleanup -p -r $~/prj.git ./ &prj.git/***
+
+# Prepare the local repository.
+#
+# Set the origin repository location as a relative path, so we can copy the
+# local/remote repository pair into the testscript scopes.
+#
+gp = $g -C prj
+
++$gp config user.name 'Test Script'
++$gp config user.email 'testscript@example.com'
++$gp remote add origin ../prj.git
++$gp add '*'
++$gp commit -m 'Create'
++$gp push --set-upstream origin master
+
+clone_prj = cp --no-cleanup -p -r ../prj ./ &prj/***
+clone_root_prj = cp --no-cleanup -p -r $~/prj ./ &prj/***
+
+clone_repos = $clone_prj && $clone_rep
+clone_root_repos = $clone_root_prj && $clone_root_rep
+
+release += 2>!
+
+# As a common approach we will be using a second local repository to validate
+# the result of the release operation(s), that is normally pushed to the
+# remote repository.
+#
+gp2 = $g -C prj2
+clone2 = $g clone prj.git prj2 &prj2/***
+pull2 = $gp2 pull
+log2 = $gp2 log '--pretty=format:"%d %s"'
+
+: single-pkg
+:
+{
+ test.arguments += --yes -q
+
+ : release
+ :
+ {
+ test.arguments += --push
+
+ : patch
+ :
+ {
+ $clone_root_repos;
+
+ $*;
+
+ $clone2;
+ $log2 >>:~%EOO%;
+ % \(HEAD -> master, \.*\) Change version to 0.2.0-a.0.z%d
+ (tag: v0.1.0) Release version 0.1.0
+ Create
+ EOO
+
+ cat prj2/manifest >>~%EOO%
+ %.*
+ name: prj
+ version: 0.2.0-a.0.z
+ summary: prj executable
+ %.*
+ EOO
+ }
+
+ : alpha
+ :
+ {
+ $clone_root_repos;
+
+ $* --alpha;
+
+ $clone2;
+ $log2 >>:~%EOO%
+ % \(HEAD -> master, \.*\) Change version to 0.1.0-a.1.z%d
+ (tag: v0.1.0-a.1) Release version 0.1.0-a.1
+ Create
+ EOO
+ }
+
+ : beta
+ :
+ {
+ $clone_root_repos;
+
+ $* --beta;
+
+ $clone2;
+ $log2 >>:~%EOO%
+ % \(HEAD -> master, \.*\) Change version to 0.1.0-b.1.z%d
+ (tag: v0.1.0-b.1) Release version 0.1.0-b.1
+ Create
+ EOO
+ }
+
+ : minor
+ :
+ {
+ $clone_root_repos;
+
+ $* --minor;
+
+ $clone2;
+ $log2 >>:~%EOO%
+ % \(HEAD -> master, \.*\) Change version to 0.3.0-a.0.z%d
+ (tag: v0.2.0) Release version 0.2.0
+ Create
+ EOO
+ }
+
+ : major
+ :
+ {
+ $clone_root_repos;
+
+ $* --major;
+
+ $clone2;
+ $log2 >>:~%EOO%
+ % \(HEAD -> master, \.*\) Change version to 1.1.0-a.0.z%d
+ (tag: v1.0.0) Release version 1.0.0
+ Create
+ EOO
+ }
+
+ : open
+ :
+ {
+ : beta
+ :
+ {
+ $clone_root_repos;
+
+ $* --open-beta 2>'error: --open-beta specified for final current version 0.1.0' != 0;
+
+ $* --alpha --open-beta;
+
+ $clone2;
+ $log2 >>:~%EOO%;
+ % \(HEAD -> master, \.*\) Change version to 0.1.0-b.0.z%d
+ (tag: v0.1.0-a.1) Release version 0.1.0-a.1
+ Create
+ EOO
+
+ $* --beta --open-beta;
+
+ $pull2;
+ $log2 >>:~%EOO%
+ % \(HEAD -> master, \.*\) Change version to 0.1.0-b.1.z%d
+ (tag: v0.1.0-b.1) Release version 0.1.0-b.1
+ Change version to 0.1.0-b.0.z
+ (tag: v0.1.0-a.1) Release version 0.1.0-a.1
+ Create
+ EOO
+ }
+
+ : patch
+ :
+ {
+ $clone_root_repos;
+
+ $* --open-patch;
+
+ $clone2;
+ $log2 >>:~%EOO%;
+ % \(HEAD -> master, \.*\) Change version to 0.1.1-a.0.z%d
+ (tag: v0.1.0) Release version 0.1.0
+ Create
+ EOO
+
+ $*; # --open-patch is implied for bugfix release series.
+
+ $pull2;
+ $log2 >>:~%EOO%
+ % \(HEAD -> master, \.*\) Change version to 0.1.2-a.0.z%d
+ (tag: v0.1.1) Release version 0.1.1
+ % Change version to 0.1.1-a.0.z%
+ (tag: v0.1.0) Release version 0.1.0
+ Create
+ EOO
+ }
+
+ : minor
+ :
+ {
+ $clone_root_repos;
+
+ $* --alpha --open-minor 2>'error: --open-minor specified for alpha current version 0.1.0-a.1' != 0;
+ $* --beta --open-minor 2>'error: --open-minor specified for beta current version 0.1.0-b.1' != 0;
+
+ $* --open-patch;
+ $*;
+ $* --open-minor;
+
+ $clone2;
+ $log2 >>:~%EOO%
+ % \(HEAD -> master, \.*\) Change version to 0.2.0-a.0.z%d
+ (tag: v0.1.2) Release version 0.1.2
+ Change version to 0.1.2-a.0.z
+ (tag: v0.1.1) Release version 0.1.1
+ Change version to 0.1.1-a.0.z
+ (tag: v0.1.0) Release version 0.1.0
+ Create
+ EOO
+ }
+
+ : major
+ :
+ {
+ $clone_root_repos;
+
+ $* --open-major;
+
+ $clone2;
+ $log2 >>:~%EOO%
+ % \(HEAD -> master, \.*\) Change version to 1.0.0-a.0.z%d
+ (tag: v0.1.0) Release version 0.1.0
+ Create
+ EOO
+ }
+ }
+
+ : no-open
+ :
+ {
+ $clone_root_repos;
+
+ $* --no-open;
+
+ $clone2;
+ $log2 >>:~%EOO%;
+ % \(HEAD -> master, tag: v0.1.0, \.*\) Release version 0.1.0%d
+ Create
+ EOO
+
+ $* --open;
+
+ $pull2;
+ $log2 >>:~%EOO%
+ % \(HEAD -> master, \.*\) Change version to 0.2.0-a.0.z%d
+ (tag: v0.1.0) Release version 0.1.0
+ Create
+ EOO
+ }
+
+ : no-tag
+ :
+ {
+ $clone_root_repos;
+
+ $* --no-tag;
+
+ $clone2;
+ $log2 >>:~%EOO%;
+ % \(HEAD -> master, \.*\) Change version to 0.2.0-a.0.z%d
+ Release version 0.1.0
+ Create
+ EOO
+
+ $* --tag 2>'error: current version 0.2.0-a.0.z is a snapshot' != 0
+ }
+
+ : no-tag-no-open
+ :
+ {
+ $clone_root_repos;
+
+ $* --no-tag --no-open;
+
+ $clone2;
+ $log2 >>:~%EOO%;
+ % \(HEAD -> master, \.*\) Release version 0.1.0%d
+ Create
+ EOO
+
+ $* --tag;
+
+ $pull2;
+ $log2 >>:~%EOO%;
+ % \(HEAD -> master, tag: v0.1.0, \.*\) Release version 0.1.0%d
+ Create
+ EOO
+
+ $* --open;
+
+ $pull2;
+ $log2 >>:~%EOO%
+ % \(HEAD -> master, \.*\) Change version to 0.2.0-a.0.z%d
+ (tag: v0.1.0) Release version 0.1.0
+ Create
+ EOO
+ }
+
+ : no-commit
+ :
+ {
+ $clone_root_repos;
+
+ $* --no-commit 2>'error: both --push and --no-commit specified' != 0
+ }
+
+ : validate-manifest
+ {
+ : file-value
+ :
+ {
+ $clone_root_repos;
+
+ echo 'description-file: README' >+ prj/manifest;
+ $gp commit -a -m 'Add description';
+
+ $* 2>>~%EOE% != 0;
+ %error: unable to read .+README referenced by description-file manifest value in .+manifest%
+ EOE
+
+ touch prj/README;
+ $gp add README;
+ $gp commit -m 'Add README file';
+
+ $* 2>>~%EOE% != 0;
+ %error: description-file manifest value in .+manifest references empty file .+README%
+ EOE
+
+ echo '.' >= prj/README;
+ $gp commit -a -m 'Fill README file';
+
+ $*
+ }
+
+ : unknown-value
+ :
+ {
+ $clone_root_repos;
+
+ echo 'some-file: README' >+ prj/manifest;
+ $gp commit -a -m 'Add file';
+
+ $* 2>>~%EOE% != 0
+ %error: invalid package manifest: .+manifest:.+:1: unknown name 'some-file' in package manifest%
+ EOE
+ }
+ }
+ }
+
+ : revision
+ :
+ {
+ +$clone_root_repos
+ +$* --no-open --push
+
+ +echo '' >+ prj/manifest
+ +$gp add manifest
+
+ test.arguments += --revision --push
+
+ : default
+ :
+ {
+ $clone_repos;
+
+ $* 2>>~%EOE%;
+ %Updated tag 'v0.1.0' \(was \.*\)%d
+ EOE
+
+ $clone2;
+ $log2 >>:~%EOO%
+ % \(HEAD -> master, tag: v0.1.0, \.*\) Release version 0.1.0\+1%d
+ Release version 0.1.0
+ Create
+ EOO
+ }
+
+ : no-tag
+ :
+ {
+ $clone_repos;
+
+ $* --no-tag;
+
+ $clone2;
+ $log2 >>:~%EOO%;
+ % \(HEAD -> master, \.*\) Release version 0.1.0\+1%d
+ (tag: v0.1.0) Release version 0.1.0
+ Create
+ EOO
+
+ $release --tag --push --yes -d prj;
+
+ $pull2 --tags; # Updates the existing local tag.
+ $log2 >>:~%EOO%
+ % \(HEAD -> master, tag: v0.1.0, \.*\) Release version 0.1.0\+1%d
+ Release version 0.1.0
+ Create
+ EOO
+ }
+ }
+
+ : no-commit
+ :
+ {
+ $clone_root_repos;
+
+ $* --no-commit;
+
+ $clone2;
+ $log2 >>:~%EOO%;
+ % \(HEAD -> master, .*\) Create%
+ EOO
+
+ $gp commit -a -m 'Release version';
+
+ $* --tag;
+ $* --open --no-commit;
+
+ $gp commit -a -m 'Change version';
+ $gp push origin HEAD --tags;
+
+ $pull2;
+ $log2 >>:~%EOO%
+ % \(HEAD -> master, .*\) Change version%
+ (tag: v0.1.0) Release version
+ Create
+ EOO
+ }
+}
+
+: multi-pkg
+:
+{
+ # Create the remote repository.
+ #
+ +mkdir --no-cleanup prj.git
+ +git -C prj.git init --bare 2>! >&2 &prj.git/***
+
+ # Create the local repository.
+ #
+ new += 2>-
+ +$new -t empty prj &prj/***
+ +$new -t exe --package prj -d prj
+ +$new -t lib --package libprj -d prj
+
+ +$gp config user.name 'Test Script'
+ +$gp config user.email 'testscript@example.com'
+ +$gp remote add origin ../prj.git
+ +$gp add '*'
+ +$gp commit -m 'Create'
+ +$gp push --set-upstream origin master
+
+ test.arguments += --push -q
+
+ : patch
+ :
+ {
+ $clone_repos;
+
+ $* --yes;
+
+ $clone2;
+ $log2 >>:~%EOO%;
+ % \(HEAD -> master, \.*\) Change version to 0.2.0-a.0.z%d
+ (tag: v0.1.0) Release version 0.1.0
+ Create
+ EOO
+
+ cat prj2/prj/manifest >>~%EOO%;
+ %.*
+ name: prj
+ version: 0.2.0-a.0.z
+ summary: prj executable
+ %.*
+ EOO
+
+ cat prj2/libprj/manifest >>~%EOO%
+ %.*
+ name: libprj
+ version: 0.2.0-a.0.z
+ project: prj
+ %.*
+ EOO
+ }
+
+ : multiple-revisions
+ :
+ {
+ test.arguments += --yes
+
+ $clone_repos;
+ $* --no-open;
+
+ echo '' >+ prj/prj/manifest;
+ $gp add prj/manifest;
+
+ $release --revision -q --yes -d prj/prj;
+
+ echo '' >+ prj/prj/manifest;
+ $gp add prj/manifest;
+
+ echo '' >+ prj/libprj/manifest;
+ $gp add libprj/manifest;
+
+ $* --revision 2>>~%EOE%;
+ %Updated tag 'v0.1.0' \(was \.*\)%d
+ EOE
+
+ $clone2;
+ $log2 >>:~%EOO%
+ % \(HEAD -> master, tag: v0.1.0, \.*\) Release versions prj/0.1.0\+2, libprj/0.1.0\+1%d
+ Release version 0.1.0+1
+ Release version 0.1.0
+ Create
+ EOO
+ }
+
+ : prompt
+ :
+ {
+ +$clone_repos
+ sp=' '
+
+ : default
+ :
+ {
+ $clone_repos;
+
+ $* <'y' 2>>:"EOE";
+ releasing:
+ package: prj
+ current: 0.1.0-a.0.z
+ release: 0.1.0
+ open: 0.2.0-a.0.z
+
+ package: libprj
+ current: 0.1.0-a.0.z
+ release: 0.1.0
+ open: 0.2.0-a.0.z
+
+ commit: yes
+ tag: v0.1.0
+ push: yes
+ continue? [y/n]$sp
+ EOE
+
+ $clone2;
+ $log2 >>:~%EOO%
+ % \(HEAD -> master, \.*\) Change version to 0.2.0-a.0.z%d
+ (tag: v0.1.0) Release version 0.1.0
+ Create
+ EOO
+ }
+
+ : no-tag
+ :
+ {
+ $clone_repos;
+
+ $* <'y' --no-tag 2>>:"EOE"
+ releasing:
+ package: prj
+ current: 0.1.0-a.0.z
+ release: 0.1.0
+ open: 0.2.0-a.0.z
+
+ package: libprj
+ current: 0.1.0-a.0.z
+ release: 0.1.0
+ open: 0.2.0-a.0.z
+
+ commit: yes
+ tag: no
+ push: yes
+ continue? [y/n]$sp
+ EOE
+ }
+
+ : no-open
+ :
+ {
+ $clone_repos;
+
+ $* <'y' --no-open 2>>:"EOE"
+ releasing:
+ package: prj
+ current: 0.1.0-a.0.z
+ release: 0.1.0
+
+ package: libprj
+ current: 0.1.0-a.0.z
+ release: 0.1.0
+
+ commit: yes
+ tag: v0.1.0
+ push: yes
+ continue? [y/n]$sp
+ EOE
+ }
+ }
+}
+
+: options-incompatibility
+:
+{
+ $clone_prj;
+
+ $* --revision --open 2>'error: both --revision and --open specified' != 0;
+ $* --revision --alpha 2>'error: both --revision and --alpha specified' != 0;
+ $* --revision --no-open 2>'error: both --revision and --no-open specified' != 0;
+ $* --revision --open-beta 2>'error: both --revision and --open-beta specified' != 0;
+ $* --open --no-tag 2>'error: both --open and --no-tag specified' != 0;
+ $* --tag --no-commit 2>'error: both --tag and --no-commit specified' != 0;
+ $* --push --no-commit 2>'error: both --push and --no-commit specified' != 0
+}