Add 'spack blame' command: shows contributors to packages
`spack blame` prints out the contributors to a package. By modification time: ``` $ spack blame --time llvm LAST_COMMIT LINES % AUTHOR EMAIL 3 days ago 2 0.6 Andrey Prokopenko <andrey.prok@gmail.com> 3 weeks ago 125 34.7 Massimiliano Culpo <massimiliano.culpo@epfl.ch> 3 weeks ago 3 0.8 Peter Scheibel <scheibel1@llnl.gov> 2 months ago 21 5.8 Adam J. Stewart <ajstewart426@gmail.com> 2 months ago 1 0.3 Gregory Becker <becker33@llnl.gov> 3 months ago 116 32.2 Todd Gamblin <tgamblin@llnl.gov> 5 months ago 2 0.6 Jimmy Tang <jcftang@gmail.com> 5 months ago 6 1.7 Jean-Paul Pelteret <jppelteret@gmail.com> 7 months ago 65 18.1 Tom Scogland <tscogland@llnl.gov> 11 months ago 13 3.6 Kelly (KT) Thompson <kgt@lanl.gov> a year ago 1 0.3 Scott Pakin <pakin@lanl.gov> a year ago 3 0.8 Erik Schnetter <schnetter@gmail.com> 3 years ago 2 0.6 David Beckingsale <davidbeckingsale@gmail.com> 3 days ago 360 100.0 ``` Or by percent contribution: ``` $ spack blame --percent llvm LAST_COMMIT LINES % AUTHOR EMAIL 3 weeks ago 125 34.7 Massimiliano Culpo <massimiliano.culpo@epfl.ch> 3 months ago 116 32.2 Todd Gamblin <tgamblin@llnl.gov> 7 months ago 65 18.1 Tom Scogland <tscogland@llnl.gov> 2 months ago 21 5.8 Adam J. Stewart <ajstewart426@gmail.com> 11 months ago 13 3.6 Kelly (KT) Thompson <kgt@lanl.gov> 5 months ago 6 1.7 Jean-Paul Pelteret <jppelteret@gmail.com> 3 weeks ago 3 0.8 Peter Scheibel <scheibel1@llnl.gov> a year ago 3 0.8 Erik Schnetter <schnetter@gmail.com> 3 years ago 2 0.6 David Beckingsale <davidbeckingsale@gmail.com> 3 days ago 2 0.6 Andrey Prokopenko <andrey.prok@gmail.com> 5 months ago 2 0.6 Jimmy Tang <jcftang@gmail.com> 2 months ago 1 0.3 Gregory Becker <becker33@llnl.gov> a year ago 1 0.3 Scott Pakin <pakin@lanl.gov> 3 days ago 360 100.0 ```
This commit is contained in:
parent
46d5901770
commit
41a2652ef2
@ -27,6 +27,7 @@
|
||||
import functools
|
||||
import collections
|
||||
import inspect
|
||||
from datetime import datetime
|
||||
from six import string_types
|
||||
|
||||
# Ignore emacs backups when listing modules
|
||||
@ -385,6 +386,75 @@ def dedupe(sequence):
|
||||
seen.add(x)
|
||||
|
||||
|
||||
def pretty_date(time, now=None):
|
||||
"""Convert a datetime or timestamp to a pretty, relative date.
|
||||
|
||||
Args:
|
||||
time (datetime or int): date to print prettily
|
||||
now (datetime): dateimte for 'now', i.e. the date the pretty date
|
||||
is relative to (default is datetime.now())
|
||||
|
||||
Returns:
|
||||
(str): pretty string like 'an hour ago', 'Yesterday',
|
||||
'3 months ago', 'just now', etc.
|
||||
|
||||
Adapted from https://stackoverflow.com/questions/1551382.
|
||||
|
||||
"""
|
||||
if now is None:
|
||||
now = datetime.now()
|
||||
|
||||
if type(time) is int:
|
||||
diff = now - datetime.fromtimestamp(time)
|
||||
elif isinstance(time, datetime):
|
||||
diff = now - time
|
||||
else:
|
||||
raise ValueError("pretty_date requires a timestamp or datetime")
|
||||
|
||||
second_diff = diff.seconds
|
||||
day_diff = diff.days
|
||||
|
||||
if day_diff < 0:
|
||||
return ''
|
||||
|
||||
if day_diff == 0:
|
||||
if second_diff < 10:
|
||||
return "just now"
|
||||
if second_diff < 60:
|
||||
return str(second_diff) + " seconds ago"
|
||||
if second_diff < 120:
|
||||
return "a minute ago"
|
||||
if second_diff < 3600:
|
||||
return str(second_diff / 60) + " minutes ago"
|
||||
if second_diff < 7200:
|
||||
return "an hour ago"
|
||||
if second_diff < 86400:
|
||||
return str(second_diff / 3600) + " hours ago"
|
||||
if day_diff == 1:
|
||||
return "yesterday"
|
||||
if day_diff < 7:
|
||||
return str(day_diff) + " days ago"
|
||||
if day_diff < 28:
|
||||
weeks = day_diff / 7
|
||||
if weeks == 1:
|
||||
return "a week ago"
|
||||
else:
|
||||
return str(day_diff / 7) + " weeks ago"
|
||||
if day_diff < 365:
|
||||
months = day_diff / 30
|
||||
if months == 1:
|
||||
return "a month ago"
|
||||
elif months == 12:
|
||||
months -= 1
|
||||
return str(months) + " months ago"
|
||||
|
||||
diff = day_diff / 365
|
||||
if diff == 1:
|
||||
return "a year ago"
|
||||
else:
|
||||
return str(diff) + " years ago"
|
||||
|
||||
|
||||
class RequiredAttributeError(ValueError):
|
||||
|
||||
def __init__(self, message):
|
||||
|
@ -32,6 +32,7 @@
|
||||
from llnl.util.lang import *
|
||||
from llnl.util.tty.colify import *
|
||||
from llnl.util.tty.color import *
|
||||
from llnl.util.filesystem import working_dir
|
||||
|
||||
import spack
|
||||
import spack.config
|
||||
@ -287,3 +288,9 @@ def fmt(s):
|
||||
raise ValueError(
|
||||
"Invalid mode for display_specs: %s. Must be one of (paths,"
|
||||
"deps, short)." % mode)
|
||||
|
||||
|
||||
def spack_is_git_repo():
|
||||
"""Ensure that this instance of Spack is a git clone."""
|
||||
with working_dir(spack.prefix):
|
||||
return os.path.isdir('.git')
|
||||
|
122
lib/spack/spack/cmd/blame.py
Normal file
122
lib/spack/spack/cmd/blame.py
Normal file
@ -0,0 +1,122 @@
|
||||
##############################################################################
|
||||
# Copyright (c) 2013-2017, Lawrence Livermore National Security, LLC.
|
||||
# Produced at the Lawrence Livermore National Laboratory.
|
||||
#
|
||||
# This file is part of Spack.
|
||||
# Created by Todd Gamblin, tgamblin@llnl.gov, All rights reserved.
|
||||
# LLNL-CODE-647188
|
||||
#
|
||||
# For details, see https://github.com/llnl/spack
|
||||
# Please also see the NOTICE and LICENSE files for our notice and the LGPL.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License (as
|
||||
# published by the Free Software Foundation) version 2.1, February 1999.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the terms and
|
||||
# conditions of the GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
##############################################################################
|
||||
import re
|
||||
|
||||
import llnl.util.tty as tty
|
||||
from llnl.util.lang import pretty_date
|
||||
from llnl.util.filesystem import working_dir
|
||||
from llnl.util.tty.colify import colify_table
|
||||
|
||||
import spack
|
||||
from spack.util.executable import which
|
||||
from spack.cmd import spack_is_git_repo
|
||||
|
||||
|
||||
description = "show contributors to packages"
|
||||
section = "developer"
|
||||
level = "long"
|
||||
|
||||
|
||||
def setup_parser(subparser):
|
||||
view_group = subparser.add_mutually_exclusive_group()
|
||||
view_group.add_argument(
|
||||
'-t', '--time', dest='view', action='store_const', const='time',
|
||||
default='time', help='sort by last modification date (default)')
|
||||
view_group.add_argument(
|
||||
'-p', '--percent', dest='view', action='store_const', const='percent',
|
||||
help='sort by percent of code')
|
||||
view_group.add_argument(
|
||||
'-g', '--git', dest='view', action='store_const', const='git',
|
||||
help='show git blame output instead of summary')
|
||||
|
||||
subparser.add_argument(
|
||||
'package_name', help='name of package to show contributions for')
|
||||
|
||||
|
||||
def blame(parser, args):
|
||||
# make sure this is a git repo
|
||||
if not spack_is_git_repo():
|
||||
tty.die("This spack is not a git clone. Can't use 'spack pkg'")
|
||||
git = which('git', required=True)
|
||||
|
||||
# Get package and package file name
|
||||
pkg = spack.repo.get(args.package_name)
|
||||
package_py = pkg.module.__file__.rstrip('c') # .pyc -> .py
|
||||
|
||||
# get git blame for the package
|
||||
with working_dir(spack.prefix):
|
||||
if args.view == 'git':
|
||||
git('blame', package_py)
|
||||
return
|
||||
else:
|
||||
output = git('blame', '--line-porcelain', package_py, output=str)
|
||||
lines = output.split('\n')
|
||||
|
||||
# Histogram authors
|
||||
counts = {}
|
||||
emails = {}
|
||||
last_mod = {}
|
||||
total_lines = 0
|
||||
for line in lines:
|
||||
match = re.match(r'^author (.*)', line)
|
||||
if match:
|
||||
author = match.group(1)
|
||||
|
||||
match = re.match(r'^author-mail (.*)', line)
|
||||
if match:
|
||||
email = match.group(1)
|
||||
|
||||
match = re.match(r'^author-time (.*)', line)
|
||||
if match:
|
||||
mod = int(match.group(1))
|
||||
last_mod[author] = max(last_mod.setdefault(author, 0), mod)
|
||||
|
||||
# ignore comments
|
||||
if re.match(r'^\t[^#]', line):
|
||||
counts[author] = counts.setdefault(author, 0) + 1
|
||||
emails.setdefault(author, email)
|
||||
total_lines += 1
|
||||
|
||||
if args.view == 'time':
|
||||
rows = sorted(
|
||||
counts.items(), key=lambda t: last_mod[t[0]], reverse=True)
|
||||
else: # args.view == 'percent'
|
||||
rows = sorted(counts.items(), key=lambda t: t[1], reverse=True)
|
||||
|
||||
# Print a nice table with authors and emails
|
||||
table = [['LAST_COMMIT', 'LINES', '%', 'AUTHOR', 'EMAIL']]
|
||||
for author, nlines in rows:
|
||||
table += [[
|
||||
pretty_date(last_mod[author]),
|
||||
nlines,
|
||||
round(nlines / float(total_lines) * 100, 1),
|
||||
author,
|
||||
emails[author]]]
|
||||
|
||||
table += [[''] * 5]
|
||||
table += [[pretty_date(max(last_mod.values())), total_lines, '100.0'] +
|
||||
[''] * 3]
|
||||
|
||||
colify_table(table)
|
@ -29,9 +29,11 @@
|
||||
import argparse
|
||||
import llnl.util.tty as tty
|
||||
from llnl.util.tty.colify import colify
|
||||
from llnl.util.filesystem import working_dir
|
||||
|
||||
import spack
|
||||
from spack.util.executable import *
|
||||
from spack.cmd import spack_is_git_repo
|
||||
|
||||
description = "query packages associated with particular git revisions"
|
||||
section = "developer"
|
||||
@ -75,26 +77,14 @@ def setup_parser(subparser):
|
||||
help="revision to compare to rev1 (default is HEAD)")
|
||||
|
||||
|
||||
def get_git(fatal=True):
|
||||
# cd to spack prefix to do git operations
|
||||
os.chdir(spack.prefix)
|
||||
|
||||
# If this is a non-git version of spack, give up.
|
||||
if not os.path.isdir('.git'):
|
||||
if fatal:
|
||||
tty.die("No git repo in %s. Can't use 'spack pkg'" % spack.prefix)
|
||||
else:
|
||||
return None
|
||||
|
||||
return which("git", required=True)
|
||||
|
||||
|
||||
def list_packages(rev):
|
||||
git = get_git()
|
||||
pkgpath = os.path.join(spack.packages_path, 'packages')
|
||||
relpath = pkgpath[len(spack.prefix + os.path.sep):] + os.path.sep
|
||||
output = git('ls-tree', '--full-tree', '--name-only', rev, relpath,
|
||||
output=str)
|
||||
|
||||
git = which('git', required=True)
|
||||
with working_dir(spack.prefix):
|
||||
output = git('ls-tree', '--full-tree', '--name-only', rev, relpath,
|
||||
output=str)
|
||||
return sorted(line[len(relpath):] for line in output.split('\n') if line)
|
||||
|
||||
|
||||
@ -105,8 +95,9 @@ def pkg_add(args):
|
||||
tty.die("No such package: %s. Path does not exist:" %
|
||||
pkg_name, filename)
|
||||
|
||||
git = get_git()
|
||||
git('-C', spack.packages_path, 'add', filename)
|
||||
git = which('git', required=True)
|
||||
with working_dir(spack.prefix):
|
||||
git('-C', spack.packages_path, 'add', filename)
|
||||
|
||||
|
||||
def pkg_list(args):
|
||||
@ -150,6 +141,9 @@ def pkg_added(args):
|
||||
|
||||
|
||||
def pkg(parser, args):
|
||||
if not spack_is_git_repo():
|
||||
tty.die("This spack is not a git clone. Can't use 'spack pkg'")
|
||||
|
||||
action = {'add': pkg_add,
|
||||
'diff': pkg_diff,
|
||||
'list': pkg_list,
|
||||
|
@ -31,7 +31,7 @@
|
||||
from llnl.util.filesystem import *
|
||||
|
||||
import spack
|
||||
from spack.cmd.pkg import get_git
|
||||
from spack.cmd import spack_is_git_repo
|
||||
from spack.util.executable import *
|
||||
|
||||
|
||||
@ -64,12 +64,15 @@ def git_case_consistency_check(path):
|
||||
TODO: lowercase for a long while.
|
||||
|
||||
"""
|
||||
with working_dir(path):
|
||||
# Don't bother fixing case if Spack isn't in a git repository
|
||||
git = get_git(fatal=False)
|
||||
if git is None:
|
||||
return
|
||||
# Don't bother fixing case if Spack isn't in a git repository
|
||||
if not spack_is_git_repo():
|
||||
return
|
||||
|
||||
git = which('git', required=False)
|
||||
if not git:
|
||||
return
|
||||
|
||||
with working_dir(path):
|
||||
try:
|
||||
git_filenames = git('ls-tree', '--name-only', 'HEAD', output=str)
|
||||
git_filenames = set(re.split(r'\s+', git_filenames.strip()))
|
||||
|
59
lib/spack/spack/test/cmd/blame.py
Normal file
59
lib/spack/spack/test/cmd/blame.py
Normal file
@ -0,0 +1,59 @@
|
||||
##############################################################################
|
||||
# Copyright (c) 2013-2017, Lawrence Livermore National Security, LLC.
|
||||
# Produced at the Lawrence Livermore National Laboratory.
|
||||
#
|
||||
# This file is part of Spack.
|
||||
# Created by Todd Gamblin, tgamblin@llnl.gov, All rights reserved.
|
||||
# LLNL-CODE-647188
|
||||
#
|
||||
# For details, see https://github.com/llnl/spack
|
||||
# Please also see the NOTICE and LICENSE files for our notice and the LGPL.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License (as
|
||||
# published by the Free Software Foundation) version 2.1, February 1999.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the terms and
|
||||
# conditions of the GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
##############################################################################
|
||||
import pytest
|
||||
|
||||
import spack.cmd
|
||||
from spack.main import SpackCommand
|
||||
from spack.util.executable import which
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not which('git') or not spack.cmd.spack_is_git_repo(),
|
||||
reason="needs git")
|
||||
|
||||
blame = SpackCommand('blame')
|
||||
|
||||
|
||||
def test_blame_by_modtime(builtin_mock):
|
||||
"""Sanity check the blame command to make sure it works."""
|
||||
out = blame('--time', 'mpich')
|
||||
assert 'LAST_COMMIT' in out
|
||||
assert 'AUTHOR' in out
|
||||
assert 'EMAIL' in out
|
||||
|
||||
|
||||
def test_blame_by_percent(builtin_mock):
|
||||
"""Sanity check the blame command to make sure it works."""
|
||||
out = blame('--percent', 'mpich')
|
||||
assert 'LAST_COMMIT' in out
|
||||
assert 'AUTHOR' in out
|
||||
assert 'EMAIL' in out
|
||||
|
||||
|
||||
def test_blame_by_git(builtin_mock, capfd):
|
||||
"""Sanity check the blame command to make sure it works."""
|
||||
with capfd.disabled():
|
||||
out = blame('--git', 'mpich')
|
||||
assert 'Mpich' in out
|
||||
assert 'mock_packages' in out
|
Loading…
Reference in New Issue
Block a user