blob: 91db3ea3970e51c30863c3d73998089d65cb2cc1 [file] [log] [blame]
# -*- coding: utf-8 -
# Copyright 2019 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.
"""Determine the time between ebuild uprevs over some time range.
TODO(evanhernandez): Move this to scripts/ once it has been hardened.
"""
from __future__ import division
from __future__ import print_function
import collections
import datetime
import os
from chromite.lib import commandline
from chromite.lib import cros_build_lib
from chromite.lib import cros_logging as logging
from chromite.lib import git
from chromite.lib import portage_util
from chromite.scripts import cros_mark_as_stable
DATE_FORMAT = '%Y-%m-%d'
SECONDS_PER_DAY = 60 * 60 * 24
# A minimal representation of a git commit.
#
# Fields:
# - id (str): The commit hash.
# - timestamp (str): The unix timestamp of the commit.
# - subject (str): The commit subject (i.e. first line of commit message).
Commit = collections.namedtuple('Commit', ['id', 'timestamp', 'subject'])
def get_directory_commits(directory, start_date=None, end_date=None):
"""Get all commits in the given directory.
Args:
directory (str): The directory in question. Must be in a git project.
start_date (datetime.datetime): The earliest datetime to consider, if any.
end_date (datetime.datetime): The latest datetime to consider, if any.
Returns:
list[Commits]: The commits relating to that directory.
"""
# TODO(evanhernandez): I am not sure how --after/--until consider timezones.
# For a script like this, the differences are probably negligible, but I
# would be happier if it guaranteed correctness.
start_date = start_date and start_date.strftime(DATE_FORMAT)
end_date = end_date and end_date.strftime(DATE_FORMAT)
output = git.Log(directory, format='format:"%h|%cd|%s"', after=start_date,
until=end_date, reverse=True, date='unix', paths=[directory])
if not output:
return []
logging.debug(output)
commit_lines = [l.strip() for l in output.splitlines() if l.strip()]
return [Commit(*cl.split('|', 2)) for cl in commit_lines]
def get_uprev_commits(commits):
"""Find all uprev commits amongs the given git commits.
Preserves order.
Args:
commits (list[Commit]): The git commits in question.
Returns:
list[Commit]: Only commits that were uprevs of some ebuild.
"""
uprev_commits = []
for commit in commits:
# Only return the ones with the cros_mark_as_stable commit message.
# TODO(evanhernandez): Yuck. Need a more robust method here...
if cros_mark_as_stable.GIT_COMMIT_SUBJECT in commit.subject:
logging.debug('Found uprev commit: %s|%s', commit.id, commit.subject)
uprev_commits.append(commit)
return uprev_commits
def get_commit_timestamps(commits):
"""Get all commit timestamps for the given ebuild.
Args:
commits (list[Commit]): The commits in question.
Returns:
list[int]: The uprev commit unix timestamps, in order.
"""
return [int(commit.timestamp) for commit in commits]
def get_average_timestamp_delta_days(timestamps):
"""Return the delta in seconds between each consecutive timestamp.
Args:
timestamps (list[int]): The unix timestamps.
Returns:
list[int]: The deltas (in seconds) between consecutive timestamps.
"""
if not len(timestamps) > 2:
raise ValueError(
'Need >= 2 timestamps to compute deltas, found: %r' % timestamps)
deltas = []
for first, second in zip(timestamps, timestamps[1:]):
delta = second - first
assert delta > 0, 'Unexpected negative delta between uprevs.'
deltas.append(delta)
average_delta_seconds = sum(deltas) / len(deltas)
return average_delta_seconds // SECONDS_PER_DAY
def get_parser():
"""Returns the argparse parser."""
parser = commandline.ArgumentParser(description=__doc__)
parser.add_argument('board', help='Board of interest.')
parser.add_argument('package', help='The package of interest.')
parse_datetime = lambda s: datetime.datetime.strptime(s, DATE_FORMAT)
parser.add_argument(
'-s', '--start-date',
type=parse_datetime,
help='Earliest date to consider uprevs. Defaults to beginning of time.')
parser.add_argument(
'-e', '--end-date', type=parse_datetime,
help='Latest date to consider uprevs. Defaults to present day.')
return parser
def main(argv):
parser = get_parser()
options = parser.parse_args(argv)
options.Freeze()
board = options.board
package = options.package
ebuild_path = portage_util.FindEbuildForBoardPackage(package, board)
if not ebuild_path:
cros_build_lib.Die('Could not find package %s for board %s.',
package, board)
logging.info('Found corresponding ebuild at: %s', ebuild_path)
ebuild = portage_util.EBuild(ebuild_path)
start_date = options.start_date
end_date = options.end_date
if start_date and end_date and start_date > end_date:
cros_build_lib.Die('Start date must be before end date.')
ebuild_commits = get_directory_commits(
os.path.dirname(ebuild.ebuild_path), start_date=start_date,
end_date=end_date)
logging.info('Found %d commits for ebuild.', len(ebuild_commits))
ebuild_uprev_commits = get_uprev_commits(ebuild_commits)
ebuild_uprev_commit_count = len(ebuild_uprev_commits)
logging.info('%d of those commits were uprevs.', ebuild_uprev_commit_count)
if ebuild_uprev_commit_count < 2:
cros_build_lib.Die(
'Alas, you need at least 2 uprevs to compute uprev frequency. '
'Try setting a larger time range?',
ebuild_uprev_commit_count)
ebuild_uprev_timestamps = get_commit_timestamps(ebuild_uprev_commits)
average_delta_days = get_average_timestamp_delta_days(ebuild_uprev_timestamps)
logging.info(
'Package %s for %s was upreved every %.2f days on average.',
ebuild.package, board, average_delta_days)