aboutsummaryrefslogtreecommitdiff
path: root/build2/version/snapshot-git.cxx
blob: bf863ce967e668d973bdb9d159c9e076b131ad05 (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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// file      : build2/version/snapshot-git.cxx -*- C++ -*-
// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
// license   : MIT; see accompanying LICENSE file

#include <libbutl/sha1.mxx>

#include <build2/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};

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

      // 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 ...
      //
      string data;

      const char* args[] {
        "git", "-C", d, "cat-file", "commit", "HEAD", nullptr};
      process pr (run_start (3 /* verbosity */, args, -1 /* stdout */));

      try
      {
        ifdstream is (move (pr.in_ofd), ifdstream::badbit);

        for (string l; !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).
            //
            size_t p1 (l.rfind (' ')); // Can't be npos.
            string tz (l, p1 + 1);

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

            string ts (l, 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 '" << l << "': "
                 << e;
          }
        }

        is.close ();
      }
      catch (const io_error&)
      {
        // Presumably the child process failed. Let run_finish() deal with
        // that.
      }

      run_finish (args, pr);

      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 (), 16); // 16-characters abbreviated commit id.

      return r;
    }
  }
}