From 10ec1ac16b5ec2577b94e8311e48ee3c7db9fb78 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Tue, 20 Mar 2018 14:01:38 +0200 Subject: Come up with better version for uncommitted snapshots Instead of leaving it as .z we now take the date of the previous commit and increment it by a second. The main benefit of doing it this way is that once committed, the new version does not "jump back" behind .z. --- build2/version/init.cxx | 20 +++++++++------- build2/version/module.hxx | 10 ++++++-- build2/version/snapshot-git.cxx | 51 +++++++++++++++++++++++++++++----------- build2/version/snapshot.hxx | 1 + doc/manual.cli | 52 ++++++++++++++++++++++++----------------- 5 files changed, 90 insertions(+), 44 deletions(-) diff --git a/build2/version/init.cxx b/build2/version/init.cxx index 7b8bd01..f793c98 100644 --- a/build2/version/init.cxx +++ b/build2/version/init.cxx @@ -163,9 +163,9 @@ namespace build2 } // If this is the latest snapshot (i.e., the -a.1.z kind), then load the - // snapshot sn and id (e.g., commit date and id from git). If there is - // uncommitted stuff, then leave it as .z. + // snapshot number and id (e.g., commit date and id from git). // + bool committed (true); if (v.snapshot () && v.snapshot_sn == standard_version::latest_sn) { snapshot ss (extract_snapshot (rs)); @@ -174,7 +174,10 @@ namespace build2 { v.snapshot_sn = ss.sn; v.snapshot_id = move (ss.id); + committed = ss.committed; } + else + committed = false; } // Set all the version.* variables. @@ -215,16 +218,17 @@ namespace build2 set ("version.pre_release_string", v.string_pre_release ()); set ("version.pre_release_number", uint64_t (v.pre_release ())); - set ("version.snapshot", v.snapshot ()); // bool - set ("version.snapshot_sn", v.snapshot_sn); // uint64 - set ("version.snapshot_id", v.snapshot_id); // string - set ("version.snapshot_string", v.string_snapshot ()); + set ("version.snapshot", v.snapshot ()); // bool + set ("version.snapshot_sn", v.snapshot_sn); // uint64 + set ("version.snapshot_id", v.snapshot_id); // string + set ("version.snapshot_string", v.string_snapshot ()); + set ("version.snapshot_committed", committed); // bool set ("version.revision", uint64_t (v.revision)); // Create the module. // - mod.reset (new module (move (v), move (ds))); + mod.reset (new module (move (v), committed, move (ds))); return true; // Init first (dist.package, etc). } @@ -331,7 +335,7 @@ namespace build2 // Complain if this is an uncommitted snapshot. // - if (v.snapshot_sn == standard_version::latest_sn) + if (v.snapshot () && !m.committed) fail << "distribution of uncommitted project " << rs.src_path (); // The plan is simple, re-serialize the manifest into a temporary file diff --git a/build2/version/module.hxx b/build2/version/module.hxx index 190d8d7..a5a667a 100644 --- a/build2/version/module.hxx +++ b/build2/version/module.hxx @@ -25,13 +25,19 @@ namespace build2 static const string name; butl::standard_version version; + bool committed; // Whether this is a committed snapshot. + dependency_constraints dependencies; const variable* in_symbol = nullptr; // in.symbol const variable* in_substitution = nullptr; // in.substitution - module (butl::standard_version v, dependency_constraints d) - : version (move (v)), dependencies (move (d)) {} + module (butl::standard_version v, + bool c, + dependency_constraints d) + : version (move (v)), + committed (c), + dependencies (move (d)) {} }; } } diff --git a/build2/version/snapshot-git.cxx b/build2/version/snapshot-git.cxx index b85b47e..751686e 100644 --- a/build2/version/snapshot-git.cxx +++ b/build2/version/snapshot-git.cxx @@ -31,9 +31,9 @@ namespace build2 // { const char* args[] {"git", "-C", d, "status", "--porcelain", nullptr}; - - if (!run (3, args, [](string& s) {return move (s);}).empty ()) - return r; + r.committed = run (3 /* verbosity */, + args, + [](string& s) {return move (s);}).empty (); } // Now extract the commit id and date. One might think that would be @@ -45,7 +45,18 @@ namespace build2 // // Where is the size of and is the output of: // - // git cat-file commit ... + // git cat-file commit HEAD + // + // There is also one annoying special case: new repository without any + // commits. In this case the above command will fail (with diagnostics + // and non-zero exit code) because there is no HEAD. Of course, it can + // also fail for other reason (like broken repository) which would be + // hard to distinguish. Note, however, that we just ran git status and + // it would have most likely failed if this were the case. So here we + // (reluctantly) assume that the only reason git cat-file fails is if + // there is no HEAD (that we equal with the "new repository" condition + // which is, strictly speaking, might not be the case either). So we + // suppress any diagnostics, and handle non-zero exit code. // string data; @@ -53,14 +64,16 @@ namespace build2 "git", "-C", d, "cat-file", "commit", "HEAD", nullptr}; process pr (run_start (3 /* verbosity */, args, - 0 /* stdin */, - -1 /* stdout */)); + 0 /* stdin */, + -1 /* stdout */, + false /* error */)); + string l; try { ifdstream is (move (pr.in_ofd), ifdstream::badbit); - for (string l; !eof (getline (is, l)); ) + while (!eof (getline (is, l))) { data += l; data += '\n'; // We assume there is always a newline. @@ -128,16 +141,28 @@ namespace build2 // that. } - run_finish (args, pr); + if (!run_finish (args, pr, false /* error */, l)) + { + // Presumably new repository without HEAD. Return uncommitted snapshot + // with UNIX epoch as timestamp. + // + r.sn = 19700101000000ULL; + r.committed = false; + return r; + } if (r.sn == 0) fail << "unable to extract git commit id/date for " << src_root; - sha1 cs; - cs.append ("commit " + to_string (data.size ())); // Includes '\0'. - cs.append (data.c_str (), data.size ()); - - r.id.assign (cs.string (), 12); // 12-characters abbreviated commit id. + if (r.committed) + { + sha1 cs; + cs.append ("commit " + to_string (data.size ())); // Includes '\0'. + cs.append (data.c_str (), data.size ()); + r.id.assign (cs.string (), 12); // 12-characters abbreviated commit id. + } + else + r.sn++; // Add a second. return r; } diff --git a/build2/version/snapshot.hxx b/build2/version/snapshot.hxx index b7e6dd7..15f4b59 100644 --- a/build2/version/snapshot.hxx +++ b/build2/version/snapshot.hxx @@ -18,6 +18,7 @@ namespace build2 { uint64_t sn = 0; string id; + bool committed = false; bool empty () const {return sn == 0;} diff --git a/doc/manual.cli b/doc/manual.cli index 6dd7002..f10e54f 100644 --- a/doc/manual.cli +++ b/doc/manual.cli @@ -343,8 +343,9 @@ increment \i{patch} when making binary-compatible changes, \i{minor} when making source-compatible changes, and \i{major} when making breaking changes. While the binary compatibility must be set in stone, the source compatibility rules can sometimes be bent. For example, you may decide to make a breaking -change in a rarely used interface as part of a minor release. Note also that -in the context of C++ deciding whether a change is binary-compatible is a +change in a rarely used interface as part of a minor release (though this is +probably still a bad idea if your library is widely depended upon). Note also +that in the context of C++ deciding whether a change is binary-compatible is a non-trivial task. There are resources that list the rules but no automated tooling yet. If unsure, increment \i{minor}. @@ -379,10 +380,11 @@ worse, from the final release. One way to remedy this is to increment the pre-release number before each publication. However, unless automated, this will be burdensome and error-prone. Also, there is a real possibility of running out of version numbers if, for example, we do continuous integration -by testing (and therefore publishing) each commit. +by publishing and testing each commit. To address this, the standard versioning scheme supports \i{snapshot -pre-releases} with the \i{prerel} component having the following form: +pre-releases} with the \i{prerel} component having the following extended +form: \ (a|b)..[.] @@ -391,13 +393,13 @@ pre-releases} with the \i{prerel} component having the following form: For example: \ -1.2.3-a.1.1422564055.340c0a26a5efed1f +1.2.3-a.1.20180319215815.26efe301f4a7 \ In essence, a snapshot pre-release is after the previous final release but before the next (\c{a.1} and, perhaps, \c{a.2} in the above example) and is uniquely identified by the snapshot sequence number (\i{snapsn}) and -snapshot id (\i{snapid}). +optional snapshot id (\i{snapid}). The \i{num} component has the same semantics as in the final pre-releases except that it can be \c{0}. The \i{snapsn} component should be either the @@ -408,12 +410,12 @@ uniquely identifies the snapshot. It is not required for version comparison (\i{snapsn} should be sufficient) and is included for reference. It must not be longer than 16 characters. -Where do the snapshot sn and id come from? Normally from the version control -system. For example, for \c{git}, \i{snapsn} is the commit date (as UNIX -timestamp in the UTC timezone) and \i{snapid} is a 16-character abbreviated -commit id. As discussed below, the \c{build2} \c{version} module extracts -and manages all this information automatically (the use of \c{git} commit -dates is not without limitations; see below for details). +Where do the snapshot number and id come from? Normally from the version +control system. For example, for \c{git}, \i{snapsn} is the commit date in the +\i{YYYYMMDDhhmmss} form and UTC timezone and \i{snapid} is a 12-character +abbreviated commit id. As discussed below, the \c{build2} \c{version} module +extracts and manages all this information automatically (but the use of +\c{git} commit dates is not without limitations; see below for details). The special '\c{z}' \i{snapsn} value identifies the \i{latest} or \i{uncommitted} snapshot. It is chosen to be greater than any other possible @@ -448,7 +450,7 @@ When it comes to the version control systems, the recommended workflow is as follows: The change to the final version should be the last commit in the (pre-)release. It is also a good idea to tag this commit with the project version. A commit immediately after that should change the version to a -snapshot essentially \"opening\" the repository for development. +snapshot, \"opening\" the repository for development. The project version without the snapshot part can be represented as a 64-bit decimal value comparable as integers (for example, in preprocessor @@ -517,6 +519,7 @@ as the following build system variables (which can be used in the buildfiles): [uint64] version.snapshot_sn # 1234567 [string] version.snapshot_id # deadbeef [string] version.snapshot_string # 1234567.deadbeef +[bool] version.snapshot_committed # true [uint64] version.revision # 3 \ @@ -537,11 +540,17 @@ control system used by the project. Currently only \c{git} is supported with the following semantics. If the project's source directory (\c{src_root}) is clean (that is, it does -not have any changed or untracked files), then the \c{HEAD} commit date and -id are used as the snapshot sn and id, respectively. Otherwise, the snapshot -is left in the \c{.z} form (which signals the latest/uncommitted -snapshot). While we can work with such a \c{.z} snapshot locally, preparing a -distribution of such an uncommitted snapshot is an error. +not have any changed or untracked files), then the \c{HEAD} commit date and id +are used as the snapshot number and id, respectively. + +Otherwise (that is, the project is between commits), the \c{HEAD} commit date +is incremented by one second and is used as the snapshot number with no id. +While we can work with such uncommitted snapshots locally, we should not +distribute or publish them since they are indistinguishable from each other. + +Finally, if the project does not have \c{HEAD} (that is, the project has +no commits yet), the special \c{19700101000000} (UNIX epoch) commit date is +used. The use of \c{git} commit dates for snapshot ordering has its limitations: they have one second resolution which means it is possible to create two @@ -555,9 +564,10 @@ option, nevertheless). When we prepare a distribution of a snapshot, the \c{version} module automatically adjusts the package name to include the snapshot information as -well as patches the manifest file in the distribution with the snapshot sn and -id (that is, replacing \c{.z} in the version value with the actual snapshot -information). The result is a package that is specific to this commit. +well as patches the manifest file in the distribution with the snapshot number +and id (that is, replacing \c{.z} in the version value with the actual +snapshot information). The result is a package that is specific to this +commit. Besides extracting the version information and making it available as individual components, the \c{version} module also provide rules for -- cgit v1.1