summaryrefslogtreecommitdiff
path: root/lib/spack
diff options
context:
space:
mode:
authorTodd Gamblin <tgamblin@llnl.gov>2017-09-28 10:47:08 -0700
committerTodd Gamblin <tgamblin@llnl.gov>2017-09-30 16:31:56 -0700
commit41a2652ef256c673dd457eebe8f8787ba151aeab (patch)
tree4dc14c389dc4e5ee6a7bf34fe72faac7681651fd /lib/spack
parent46d5901770010f7ad458a54fb60a3c63bb97de8c (diff)
downloadspack-41a2652ef256c673dd457eebe8f8787ba151aeab.tar.gz
spack-41a2652ef256c673dd457eebe8f8787ba151aeab.tar.bz2
spack-41a2652ef256c673dd457eebe8f8787ba151aeab.tar.xz
spack-41a2652ef256c673dd457eebe8f8787ba151aeab.zip
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 ```
Diffstat (limited to 'lib/spack')
-rw-r--r--lib/spack/llnl/util/lang.py70
-rw-r--r--lib/spack/spack/cmd/__init__.py7
-rw-r--r--lib/spack/spack/cmd/blame.py122
-rw-r--r--lib/spack/spack/cmd/pkg.py32
-rw-r--r--lib/spack/spack/hooks/case_consistency.py15
-rw-r--r--lib/spack/spack/test/cmd/blame.py59
6 files changed, 280 insertions, 25 deletions
diff --git a/lib/spack/llnl/util/lang.py b/lib/spack/llnl/util/lang.py
index 667572dd8d..07338b21ef 100644
--- a/lib/spack/llnl/util/lang.py
+++ b/lib/spack/llnl/util/lang.py
@@ -27,6 +27,7 @@ import re
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):
diff --git a/lib/spack/spack/cmd/__init__.py b/lib/spack/spack/cmd/__init__.py
index 08dbf55dc2..f1e36c2377 100644
--- a/lib/spack/spack/cmd/__init__.py
+++ b/lib/spack/spack/cmd/__init__.py
@@ -32,6 +32,7 @@ import llnl.util.tty as tty
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 display_specs(specs, args=None, **kwargs):
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')
diff --git a/lib/spack/spack/cmd/blame.py b/lib/spack/spack/cmd/blame.py
new file mode 100644
index 0000000000..3bf5e37abb
--- /dev/null
+++ b/lib/spack/spack/cmd/blame.py
@@ -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)
diff --git a/lib/spack/spack/cmd/pkg.py b/lib/spack/spack/cmd/pkg.py
index 7673955db5..8cd6534724 100644
--- a/lib/spack/spack/cmd/pkg.py
+++ b/lib/spack/spack/cmd/pkg.py
@@ -29,9 +29,11 @@ import os
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,
diff --git a/lib/spack/spack/hooks/case_consistency.py b/lib/spack/spack/hooks/case_consistency.py
index 48f9606444..b76408e66e 100644
--- a/lib/spack/spack/hooks/case_consistency.py
+++ b/lib/spack/spack/hooks/case_consistency.py
@@ -31,7 +31,7 @@ import platform
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()))
diff --git a/lib/spack/spack/test/cmd/blame.py b/lib/spack/spack/test/cmd/blame.py
new file mode 100644
index 0000000000..0031052b24
--- /dev/null
+++ b/lib/spack/spack/test/cmd/blame.py
@@ -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