+def _gitenv(repo_dir=None):
+ if not repo_dir:
+ repo_dir = repo()
+ return merge_dict(os.environ, {'GIT_DIR': os.path.abspath(repo_dir)})
+
+def _git_wait(cmd, p):
+ rv = p.wait()
+ if rv != 0:
+ raise GitError('%s returned %d' % (shstr(cmd), rv))
+
+def _git_capture(argv):
+ p = subprocess.Popen(argv, stdout=subprocess.PIPE, env=_gitenv())
+ r = p.stdout.read()
+ _git_wait(repr(argv), p)
+ return r
+
+def git_config_get(option, repo_dir=None):
+ cmd = ('git', 'config', '--get', option)
+ p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
+ env=_gitenv(repo_dir=repo_dir))
+ r = p.stdout.read()
+ rc = p.wait()
+ if rc == 0:
+ return r
+ if rc != 1:
+ raise GitError('%s returned %d' % (cmd, rc))
+ return None
+
+
+def parse_tz_offset(s):
+ """UTC offset in seconds."""
+ tz_off = (int(s[1:3]) * 60 * 60) + (int(s[3:5]) * 60)
+ if s[0] == '-':
+ return - tz_off
+ return tz_off
+
+
+# FIXME: derived from http://git.rsbx.net/Documents/Git_Data_Formats.txt
+# Make sure that's authoritative.
+_start_end_char = r'[^ .,:;<>"\'\0\n]'
+_content_char = r'[^\0\n<>]'
+_safe_str_rx = '(?:%s{1,2}|(?:%s%s*%s))' \
+ % (_start_end_char,
+ _start_end_char, _content_char, _start_end_char)
+_tz_rx = r'[-+]\d\d[0-5]\d'
+_parent_rx = r'(?:parent [abcdefABCDEF0123456789]{40}\n)'
+# Assumes every following line starting with a space is part of the
+# mergetag. Is there a formal commit blob spec?
+_mergetag_rx = r'(?:\nmergetag object [abcdefABCDEF0123456789]{40}(?:\n [^\0\n]*)*)'
+_commit_rx = re.compile(r'''tree (?P<tree>[abcdefABCDEF0123456789]{40})
+(?P<parents>%s*)author (?P<author_name>%s) <(?P<author_mail>%s)> (?P<asec>\d+) (?P<atz>%s)
+committer (?P<committer_name>%s) <(?P<committer_mail>%s)> (?P<csec>\d+) (?P<ctz>%s)(?P<mergetag>%s?)
+
+(?P<message>(?:.|\n)*)''' % (_parent_rx,
+ _safe_str_rx, _safe_str_rx, _tz_rx,
+ _safe_str_rx, _safe_str_rx, _tz_rx,
+ _mergetag_rx))
+_parent_hash_rx = re.compile(r'\s*parent ([abcdefABCDEF0123456789]{40})\s*')
+
+# Note that the author_sec and committer_sec values are (UTC) epoch
+# seconds, and for now the mergetag is not included.
+CommitInfo = namedtuple('CommitInfo', ['tree', 'parents',
+ 'author_name', 'author_mail',
+ 'author_sec', 'author_offset',
+ 'committer_name', 'committer_mail',
+ 'committer_sec', 'committer_offset',
+ 'message'])
+
+def parse_commit(content):
+ commit_match = re.match(_commit_rx, content)
+ if not commit_match:
+ raise Exception('cannot parse commit %r' % content)
+ matches = commit_match.groupdict()
+ return CommitInfo(tree=matches['tree'],
+ parents=re.findall(_parent_hash_rx, matches['parents']),
+ author_name=matches['author_name'],
+ author_mail=matches['author_mail'],
+ author_sec=int(matches['asec']),
+ author_offset=parse_tz_offset(matches['atz']),
+ committer_name=matches['committer_name'],
+ committer_mail=matches['committer_mail'],
+ committer_sec=int(matches['csec']),
+ committer_offset=parse_tz_offset(matches['ctz']),
+ message=matches['message'])
+
+
+def get_cat_data(cat_iterator, expected_type):
+ _, kind, _ = next(cat_iterator)
+ if kind != expected_type:
+ raise Exception('expected %r, saw %r' % (expected_type, kind))
+ return ''.join(cat_iterator)
+
+def get_commit_items(id, cp):
+ return parse_commit(get_cat_data(cp.get(id), 'commit'))
+
+def _local_git_date_str(epoch_sec):
+ return '%d %s' % (epoch_sec, utc_offset_str(epoch_sec))
+
+
+def _git_date_str(epoch_sec, tz_offset_sec):
+ offs = tz_offset_sec // 60
+ return '%d %s%02d%02d' \
+ % (epoch_sec,
+ '+' if offs >= 0 else '-',
+ abs(offs) // 60,
+ abs(offs) % 60)
+
+
+def repo(sub = '', repo_dir=None):