blob: 1eab1214cad03735b1d83ade5f9a166b9d91ae14 [file] [log] [blame]
# Copyright 2017 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Git repo metrics."""
import logging
import os
import subprocess
from chromite.lib import metrics
logger = logging.getLogger(__name__)
class _GitRepo:
"""Helper class for running git commands."""
def __init__(self, gitdir) -> None:
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:
"""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) -> None:
self._gitdir = gitdir
self._gitrepo = _GitRepo(os.path.expanduser(gitdir))
self._fields = {"repo": gitdir}
self._metric_path = metric_path
def collect(self) -> None:
"""Collect metrics."""
try:
self._collect_commit_hash_metric()
self._collect_timestamp_metric()
self._collect_unstaged_changes_metric()
except subprocess.CalledProcessError as e:
logger.warning(
"Error collecting git metrics for %s: %s", self._gitdir, e
)
def _collect_commit_hash_metric(self) -> None:
commit_hash = self._gitrepo.get_commit_hash()
logger.debug("Collecting Git hash %r for %r", commit_hash, self._gitdir)
self._commit_hash_metric.set(commit_hash, self._fields)
def _collect_timestamp_metric(self) -> None:
commit_time = self._gitrepo.get_commit_time()
logger.debug(
"Collecting Git timestamp %r for %r", commit_time, self._gitdir
)
self._timestamp_metric.set(commit_time, self._fields)
def _collect_unstaged_changes_metric(self) -> None:
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() -> None:
"""Collect metrics for Git repository state."""
for collector in _repo_collectors:
collector.collect()