stg import now extracts Message-ID header
[stgit.git] / stgit / lib / git / date.py
blob7e0d4ef777b5480608f6595137bbe2702068e8ad
1 import email.utils
2 import re
3 from datetime import datetime, timedelta, tzinfo
5 from stgit.exception import StgException
6 from stgit.lib.git.base import Immutable
7 from stgit.run import Run, RunException
10 class DateException(StgException):
11 """Exception raised when a date+time string could not be parsed."""
13 def __init__(self, string, type):
14 super().__init__('"%s" is not a valid %s' % (string, type))
17 class TimeZone(tzinfo):
18 """A simple time zone class for static offsets from UTC.
20 We have to define our own since Python's standard library doesn't define any time
21 zone classes.
23 """
25 def __init__(self, tzstring):
26 m = re.match(r'^([+-])(\d{2}):?(\d{2})$', tzstring)
27 if not m:
28 raise DateException(tzstring, 'time zone')
29 sign = int(m.group(1) + '1')
30 try:
31 self._offset = timedelta(
32 hours=sign * int(m.group(2)), minutes=sign * int(m.group(3))
34 except OverflowError:
35 raise DateException(tzstring, 'time zone')
36 self._name = tzstring
38 def utcoffset(self, dt):
39 return self._offset
41 def tzname(self, dt):
42 return self._name
44 def dst(self, dt):
45 return timedelta(0)
47 def __repr__(self):
48 return self._name
51 def system_date(datestring):
52 m = re.match(r"^(.+)([+-]\d\d:?\d\d)$", datestring)
53 if m:
54 # Time zone included; we parse it ourselves, since "date"
55 # would convert it to the local time zone.
56 (ds, z) = m.groups()
57 try:
58 t = Run("date", "+%Y-%m-%d-%H-%M-%S", "-d", ds).output_one_line()
59 except RunException:
60 return None
61 else:
62 # Time zone not included; we ask "date" to provide it for us.
63 try:
64 d = Run("date", "+%Y-%m-%d-%H-%M-%S_%z", "-d", datestring).output_one_line()
65 except RunException:
66 return None
67 (t, z) = d.split("_")
68 year, month, day, hour, minute, second = [int(x) for x in t.split("-")]
69 try:
70 return datetime(year, month, day, hour, minute, second, tzinfo=TimeZone(z))
71 except ValueError:
72 raise DateException(datestring, "date")
75 def git_date(datestring=''):
76 try:
77 ident = (
78 Run('git', 'var', 'GIT_AUTHOR_IDENT')
79 .env(
81 'GIT_AUTHOR_NAME': 'XXX',
82 'GIT_AUTHOR_EMAIL': 'XXX',
83 'GIT_AUTHOR_DATE': datestring,
86 .output_one_line()
88 except RunException:
89 return None
90 _, _, timestamp, offset = ident.split()
91 return datetime.fromtimestamp(int(timestamp), TimeZone(offset))
94 class Date(Immutable):
95 """Represents a timestamp used in Git commits."""
97 def __init__(self, datestring):
98 # Try git-formatted date.
99 m = re.match(r'^(\d+)\s+([+-]\d\d:?\d\d)$', datestring)
100 if m:
101 try:
102 self._time = datetime.fromtimestamp(
103 int(m.group(1)), TimeZone(m.group(2))
105 except ValueError:
106 raise DateException(datestring, 'date')
107 return
109 # Try iso-formatted date.
110 m = re.match(
111 r'^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})\s+'
112 r'([+-]\d\d:?\d\d)$',
113 datestring,
115 if m:
116 try:
117 self._time = datetime(
118 *[int(m.group(i + 1)) for i in range(6)],
119 **{'tzinfo': TimeZone(m.group(7))},
121 except ValueError:
122 raise DateException(datestring, 'date')
123 return
125 if datestring == 'now':
126 self._time = git_date()
127 assert self._time
128 return
130 # Try parsing with `git var`.
131 gd = git_date(datestring)
132 if gd:
133 self._time = gd
134 return
136 # Try parsing with the system's "date" command.
137 sd = system_date(datestring)
138 if sd:
139 self._time = sd
140 return
142 raise DateException(datestring, 'date')
144 def __repr__(self):
145 return self.isoformat()
147 def isoformat(self):
148 """Human-friendly ISO 8601 format."""
149 return '%s %s' % (
150 self._time.replace(tzinfo=None).isoformat(str(' ')),
151 self._time.tzinfo,
154 def rfc2822_format(self):
155 """Format date in RFC-2822 format, as used in email."""
156 return email.utils.format_datetime(self._time)
158 @classmethod
159 def maybe(cls, datestring):
160 """Create new :class:`Date` from non-None argument.
162 Returns None if the argument is None.
165 return cls(datestring) if datestring is not None else None