blob: 06a411f2b806ae3ca76e5481a6dd8a3496d26ae1 [file] [log] [blame]
# Copyright 2017 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Git repo metrics."""
from __future__ import absolute_import
import logging
import os
import subprocess
from chromite.lib import metrics
logger = logging.getLogger(__name__)
class _GitRepo(object):
"""Helper class for running git commands."""
def __init__(self, gitdir):
self._gitdir = gitdir
def _get_git_command(self):
return ['git', '--git-dir', self._gitdir]
def _check_output(self, args, **kwargs):
return subprocess.check_output(
self._get_git_command() + list(args), **kwargs).decode('utf-8')
def get_commit_hash(self):
"""Return commit hash string."""
return self._check_output(['rev-parse', 'HEAD']).strip()
def get_commit_time(self):
"""Return commit time as UNIX timestamp int."""
return int(self._check_output(['show', '-s', '--format=%ct', 'HEAD'])
.strip())
def get_unstaged_changes(self):
"""Return number of unstaged changes as (added, deleted)."""
added_total, deleted_total = 0, 0
# output looks like:
# '1\t2\tfoo\n3\t4\tbar\n'
# '-\t-\tbinary_file\n'
output = self._check_output(['diff-index', '--numstat', 'HEAD'])
stats_strings = (line.split() for line in output.splitlines())
for added, deleted, _path in stats_strings:
if added != '-':
added_total += int(added)
if deleted != '-':
deleted_total += int(deleted)
return added_total, deleted_total
class _GitMetricCollector(object):
"""Class for collecting metrics about a git repository.
The constructor takes the arguments: `gitdir`, `metric_path`.
`gitdir` is the path to the Git directory to collect metrics for and
may start with a tilde (expanded to a user's home directory).
`metric_path` is the Monarch metric path to report to.
"""
_commit_hash_metric = metrics.StringMetric(
'git/hash',
description='Current Git commit hash.')
_timestamp_metric = metrics.GaugeMetric(
'git/timestamp',
description='Current Git commit time as seconds since Unix Epoch.')
_unstaged_changes_metric = metrics.GaugeMetric(
'git/unstaged_changes',
description='Unstaged Git changes.')
def __init__(self, gitdir, metric_path):
self._gitdir = gitdir
self._gitrepo = _GitRepo(os.path.expanduser(gitdir))
self._fields = {'repo': gitdir}
self._metric_path = metric_path
def collect(self):
"""Collect metrics."""
try:
self._collect_commit_hash_metric()
self._collect_timestamp_metric()
self._collect_unstaged_changes_metric()
except subprocess.CalledProcessError as e:
logger.warning(u'Error collecting git metrics for %s: %s',
self._gitdir, e)
def _collect_commit_hash_metric(self):
commit_hash = self._gitrepo.get_commit_hash()
logger.debug(u'Collecting Git hash %r for %r', commit_hash, self._gitdir)
self._commit_hash_metric.set(commit_hash, self._fields)
def _collect_timestamp_metric(self):
commit_time = self._gitrepo.get_commit_time()
logger.debug(u'Collecting Git timestamp %r for %r',
commit_time, self._gitdir)
self._timestamp_metric.set(commit_time, self._fields)
def _collect_unstaged_changes_metric(self):
added, deleted = self._gitrepo.get_unstaged_changes()
self._unstaged_changes_metric.set(
added, fields=dict(change_type='added', **self._fields))
self._unstaged_changes_metric.set(
deleted, fields=dict(change_type='deleted', **self._fields))
_CHROMIUMOS_DIR = '~chromeos-test/chromiumos/'
_repo_collectors = (
# TODO(ayatane): We cannot access chromeos-admin because we are
# running as non-root.
_GitMetricCollector(gitdir='/root/chromeos-admin/.git',
metric_path='chromeos-admin'),
_GitMetricCollector(gitdir=_CHROMIUMOS_DIR + 'chromite/.git',
metric_path='chromite'),
_GitMetricCollector(gitdir='/usr/local/autotest/.git',
metric_path='installed_autotest'),
)
def collect_git_metrics():
"""Collect metrics for Git repository state."""
for collector in _repo_collectors:
collector.collect()