diff options
Diffstat (limited to 'libbuild2/version/snapshot-git.cxx')
-rw-r--r-- | libbuild2/version/snapshot-git.cxx | 175 |
1 files changed, 175 insertions, 0 deletions
diff --git a/libbuild2/version/snapshot-git.cxx b/libbuild2/version/snapshot-git.cxx new file mode 100644 index 0000000..b7ca084 --- /dev/null +++ b/libbuild2/version/snapshot-git.cxx @@ -0,0 +1,175 @@ +// file : libbuild2/version/snapshot-git.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <ctime> // time_t + +#include <libbutl/sha1.mxx> + +#include <libbuild2/version/snapshot.hxx> + +using namespace std; +using namespace butl; + +namespace build2 +{ + namespace version + { + snapshot + extract_snapshot_git (const dir_path& src_root) + { + snapshot r; + const char* d (src_root.string ().c_str ()); + + // First check whether the working directory is clean. There doesn't + // seem to be a way to do everything in a single invocation (the + // porcelain v2 gives us the commit id but not timestamp). + // + + // If git status --porcelain returns anything, then the working + // directory is not clean. + // + { + const char* args[] {"git", "-C", d, "status", "--porcelain", nullptr}; + r.committed = run<string> ( + 3 /* verbosity */, + args, + [](string& s, bool) {return move (s);}).empty (); + } + + // Now extract the commit id and date. One might think that would be + // easy... Commit id is a SHA1 hash of the commit object. And commit + // object looks like this: + // + // commit <len>\0 + // <data> + // + // Where <len> is the size of <data> and <data> is the output of: + // + // 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; + + const char* args[] { + "git", "-C", d, "cat-file", "commit", "HEAD", nullptr}; + process pr (run_start (3 /* verbosity */, + args, + 0 /* stdin */, + -1 /* stdout */, + false /* error */)); + + string l; + try + { + ifdstream is (move (pr.in_ofd), ifdstream::badbit); + + while (!eof (getline (is, l))) + { + data += l; + data += '\n'; // We assume there is always a newline. + + if (r.sn == 0 && l.compare (0, 10, "committer ") == 0) + try + { + // The line format is: + // + // committer <noise> <timestamp> <timezone> + // + // For example: + // + // committer John Doe <john@example.org> 1493117819 +0200 + // + // The timestamp is in seconds since UNIX epoch. The timezone + // appears to be always numeric (+0000 for UTC). Note that + // timestamp appears to be already in UTC with timezone being just + // for information it seems. + // + size_t p1 (l.rfind (' ')); // Can't be npos. + + size_t p2 (l.rfind (' ', p1 - 1)); + if (p2 == string::npos) + throw invalid_argument ("missing timestamp"); + + string ts (l, p2 + 1, p1 - p2 - 1); + time_t t (static_cast<time_t> (stoull (ts))); + +#if 0 + string tz (l, p1 + 1); + + if (tz.size () != 5) + throw invalid_argument ("invalid timezone"); + + unsigned long h (stoul (string (tz, 1, 2))); + unsigned long m (stoul (string (tz, 3, 2))); + unsigned long s (h * 3600 + m * 60); + + // The timezone indicates where the timestamp was generated so to + // convert to UTC we need to invert the sign. + // + switch (tz[0]) + { + case '+': t -= s; break; + case '-': t += s; break; + default: throw invalid_argument ("invalid timezone sign"); + } +#endif + // Represent as YYYYMMDDhhmmss. + // + r.sn = stoull (to_string (system_clock::from_time_t (t), + "%Y%m%d%H%M%S", + false /* special */, + false /* local (already in UTC) */)); + } + catch (const invalid_argument& e) + { + fail << "unable to extract git commit date from '" << l << "': " + << e; + } + } + + is.close (); + } + catch (const io_error&) + { + // Presumably the child process failed. Let run_finish() deal with + // that. + } + + 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; + + 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; + } + } +} |