summaryrefslogtreecommitdiff
path: root/lib/spack/spack/cmd/blame.py
blob: a4f75da19a18e29cf436ac00ec7293524668f9bd (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
##############################################################################
# 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/spack/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 os
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, '
        'or path to a file in the spack repo')


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 blame'")
    git = which('git', required=True)

    # Get name of file to blame
    blame_file = None
    if os.path.isfile(args.package_name):
        path = os.path.realpath(args.package_name)
        if path.startswith(spack.prefix):
            blame_file = path

    if not blame_file:
        pkg = spack.repo.get(args.package_name)
        blame_file = pkg.module.__file__.rstrip('c')  # .pyc -> .py

    # get git blame for the package
    with working_dir(spack.prefix):
        if args.view == 'git':
            git('blame', blame_file)
            return
        else:
            output = git('blame', '--line-porcelain', blame_file, 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)