| # 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() |