aboutsummaryrefslogtreecommitdiff
path: root/build2/version/snapshot-git.cxx
blob: b5db470b278e41e5186480e52ba33f72174ebda4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// file      : build2/version/snapshot-git.cxx -*- C++ -*-
// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
// license   : MIT; see accompanying LICENSE file

#include <build2/version/snapshot>

using namespace std;

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};

        if (!run<string> (args, [] (string& s) {return move (s);}).empty ())
          return r;
      }

      // Now extract the commit id and date.
      //
      auto extract = [&r] (string& s) -> snapshot
      {
        if (s.compare (0, 5, "tree ") == 0)
        {
          // The 16-characters abbreviated commit id.
          //
          r.id.assign (s, 5, 16);

          if (r.id.size () != 16)
            fail << "unable to extract git commit id from '" << s << "'";
        }
        else if (s.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).
          //
          size_t p1 (s.rfind (' ')); // Can't be npos.
          string tz (s, p1 + 1);

          size_t p2 (s.rfind (' ', p1 - 1));
          if (p2 == string::npos)
            throw invalid_argument ("missing timestamp");

          string ts (s, p2 + 1, p1 - p2 - 1);
          r.sn = stoull (ts);

          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 '+': r.sn -= s; break;
          case '-': r.sn += s; break;
          default: throw invalid_argument ("invalid timezone sign");
          }
        }
        catch (const invalid_argument& e)
        {
          fail << "unable to extract git commit date from '" << s << "': " << e;
        }

        return (r.id.empty () || r.sn == 0) ? snapshot () : move (r);
      };

      const char* args[] {
        "git", "-C", d, "cat-file", "commit", "HEAD", nullptr};
      r = run<snapshot> (args, extract);

      if (r.empty ())
        fail << "unable to extract git commit id/date for " << src_root;

      return r;
    }
  }
}