| # 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.""" | |
| from __future__ import absolute_import | |
| 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): | |
| 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): | |
| 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( | |
| "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("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( | |
| "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() |