1#!/usr/bin/env python3
2#
3# Copyright 2020, Data61, CSIRO (ABN 41 687 119 230)
4#
5# SPDX-License-Identifier: BSD-2-Clause
6#
7
8import argparse
9import os
10import sys
11import re
12from datetime import timedelta, date
13from dateutil.parser import isoparse
14
15DESCRIPTION = 'Run sorry count statistics on repository'
16
17
18def cmd(c):
19    return os.popen(c).read()
20
21
22def log_lines(start, end, path):
23    """
24    git log -p for the specified date period
25    """
26    git_cmd = 'git log --date=local -p --since "%s" --before "%s" -- %s'
27    return cmd(git_cmd % (start, end, path)).splitlines()
28
29
30def sorries_current(path):
31    """
32    git grep
33    """
34    git_cmd = 'git grep sorry %s | wc -l' % path
35    return int(cmd(git_cmd).strip())
36
37
38def repo_root():
39    git_cmd = 'git rev-parse --show-toplevel'
40    return cmd(git_cmd)[:-1]
41
42
43def print_stats(deadline, delta, end, path):
44    start = end - delta
45
46    cur_path = os.getcwd()
47    os.chdir(repo_root())
48    difflog = log_lines(start, end, path)
49    current = sorries_current(path)
50    os.chdir(cur_path)
51
52    sorry_added = re.compile("^\+.*sorry")
53    sorry_removed = re.compile("^-.*sorry")
54    lemma_added = re.compile("^\+.*lemma")
55    lemma_removed = re.compile("^-.*lemma")
56    author_line = re.compile("^Author:")
57    patch_line = re.compile("(^\+)|(^-)")
58
59    sorries_added = 0
60    sorries_removed = 0
61    lemmas_added = 0
62    lines = 0
63
64    authors = {}
65
66    for line in difflog:
67        if patch_line.match(line):
68            lines += 1
69        if sorry_added.match(line):
70            sorries_added += 1
71        if sorry_removed.match(line):
72            sorries_removed += 1
73        if lemma_added.match(line):
74            lemmas_added += 1
75        if lemma_removed.match(line):
76            lemmas_added -= 1
77        if author_line.match(line):
78            authors[line] = 1
79
80    balance = sorries_added-sorries_removed
81
82    removed_per_day = sorries_removed / delta.days
83    balance_per_day = balance / delta.days
84
85    if removed_per_day != 0:
86        projected_days = current / removed_per_day
87    else:
88        projected_days = -1
89
90    today = date.today()
91    projected_end = today + timedelta(days=projected_days)
92
93    days_left = deadline - today
94    needed_rate = current / days_left.days
95
96    days_diff = projected_days-days_left.days
97    if days_diff == 0:
98        over_under = 'precisely on target'
99    elif days_diff < 0:
100        over_under = '%d days early' % (-days_diff)
101    else:
102        over_under = '%d days late' % days_diff
103
104    print("Date range: {} to {} ({:.0f} weeks)".format(start, end, delta.days/7))
105    print()
106    print("Patch lines:     {:6d}".format(lines))
107    print("Lemmas added:    {:6d}".format(lemmas_added))
108    print("Sorries added:   {:6d}".format(sorries_added))
109    print("Sorries removed: {:6d}".format(sorries_removed))
110    print("Sorry balance:   {:+6d}".format(balance))
111    print("Active authors:  {:6d}".format(len(list(authors))))
112    print()
113    print("Sorries current: {:6d}".format(current))
114    print()
115    print("Rate removed:    {:6.1f} sorries per week".format(removed_per_day * 7))
116    print("Rate balance:    {:+6.1f} sorries per week".format(balance_per_day * 7))
117    print("Rate needed:     {:6.1f} sorries per week ({:+.1f} s/w)".format(
118        needed_rate * 7, (needed_rate-removed_per_day)*7))
119    print()
120    print("Target end date: {0} (in {1} days)".format(deadline, days_left.days))
121    if projected_days >= 0:
122        print("Projected date:  {0} ({1})".format(projected_end, over_under))
123    else:
124        print("Projected date:  inf")
125
126
127if __name__ == '__main__':
128    # Setup the command line parser.
129    parser = argparse.ArgumentParser(description=DESCRIPTION)
130    parser.add_argument('-w', "--weeks", help="Number of weeks to look back",
131                        type=int, default=4)
132    parser.add_argument('-e', "--end", help="End of the stats time period",
133                        default=date.today().isoformat())
134    parser.add_argument('-p', "--path", help="Restrict statistic to this path",
135                        default=".")
136    parser.add_argument("deadline", help="Project deadline (yyyy-mm-dd)")
137
138    args = parser.parse_args()
139
140    delta = timedelta(weeks=args.weeks)
141    end = isoparse(args.end).date()
142    deadline = isoparse(args.deadline).date()
143
144    print_stats(deadline, delta, end, args.path)
145    sys.exit(0)
146