Merge branch 'stable' into devel
[tails.git] / bin / generate-report
blob7e32a45ce4309a084c2d41a5011a8f2c1104cee2
1 #! /usr/bin/python3
3 # Documentation: https://tails.net/contribute/working_together/GitLab/#api
5 import functools
6 import sys
7 import logging
8 import os
9 from datetime import datetime
11 try:
12     import gitlab  # type: ignore
13 except ImportError:
14     sys.exit("You need to install python3-gitlab to use this program.")
16 try:
17     from dateutil.relativedelta import relativedelta
18 except ImportError:
19     sys.exit("You need to install python3-dateutil to use this program.")
20 from pathlib import Path
23 PYTHON_GITLAB_CONFIG_FILE = os.getenv('PYTHON_GITLAB_CONFIG_FILE',
24                                       default=Path.home() /
25                                       '.python-gitlab.cfg')
27 PYTHON_GITLAB_NAME = os.getenv('GITLAB_NAME', default='Tails')
29 GROUP_NAME = 'tails'
31 # By default, only changes in these projects are considered
32 PROJECTS = [
33     GROUP_NAME + '/' + project for project in [
34         'chutney',
35         'installer',
36         'sysadmin',
37         'tails',
38         'whisperback',
39         'workarounds',
40     ]
43 LOG_FORMAT = "%(asctime)-15s %(levelname)s %(message)s"
44 log = logging.getLogger()
47 class GitLabWrapper(gitlab.Gitlab):
48     @functools.lru_cache(maxsize=None)
49     def project(self, project_id):
50         return self.projects.get(project_id)
52     @functools.lru_cache(maxsize=None)
53     def project_from_name(self, project_name):
54         project = [
55             p for p in self.projects.list(all=True)
56             # Disambiguate between projects whose names share a common prefix
57             if p.path_with_namespace == project_name
58         ][0]
59         assert isinstance(project, gitlab.v4.objects.Project)
60         return project
63 class ReportGenerator(object):
64     def __init__(self, gl, group, projects: list, label: str, year: int,
65                  month: int):
66         self.gl = gl
67         self.group = group
68         self.projects = projects
69         self.label = label
70         self.after = end_of_previous_month(year, month)
71         self.before = beginning_of_next_month(year, month)
73     def closed_issues_in_project(self, project_name) -> list:
74         closed_issues = []
75         project = self.gl.project_from_name(project_name)
76         closed_issues_events = project.events.list(as_list=False,
77                                                    target_type='issue',
78                                                    action='closed',
79                                                    after=self.after,
80                                                    before=self.before)
82         gl_closed_issues_with_duplicates = [{
83             "project_id": event.project_id,
84             "iid": event.target_iid
85         } for event in closed_issues_events]
86         gl_closed_issues = []
87         for issue in gl_closed_issues_with_duplicates:
88             if issue not in gl_closed_issues:
89                 gl_closed_issues.append(issue)
91         for issue in gl_closed_issues:
92             project = self.gl.project(issue["project_id"])
93             issue = project.issues.get(issue["iid"])
94             if self.label is not None and self.label not in issue.labels:
95                 continue
96             closed_issues.append({
97                 "title": issue.title,
98                 "web_url": issue.web_url,
99             })
101         return closed_issues
103     def closed_issues(self) -> list:
104         closed_issues = []
105         for project in self.projects:
106             closed_issues = closed_issues + self.closed_issues_in_project(
107                 project)
108         return closed_issues
111 def beginning_of_next_month(year, month):
112     return (datetime(year, month, 1) + relativedelta(months=1)).replace(day=1)
115 def end_of_previous_month(year, month):
116     return datetime(year, month, 1) + relativedelta(seconds=-1)
119 if __name__ == '__main__':
120     import argparse
121     parser = argparse.ArgumentParser()
122     parser.add_argument('--year', type=int, required=True)
123     parser.add_argument('--month', type=int, required=True)
124     parser.add_argument('--label', default=None)
125     parser.add_argument('--project')
126     parser.add_argument("--debug", action="store_true", help="debug output")
127     args = parser.parse_args()
129     if args.debug:
130         logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT)
131     else:
132         logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)
134     gl = GitLabWrapper.from_config(PYTHON_GITLAB_NAME,
135                                    config_files=[PYTHON_GITLAB_CONFIG_FILE])
136     gl.auth()
138     group = gl.groups.list(search=GROUP_NAME)[0]
139     assert isinstance(group, gitlab.v4.objects.Group)
141     if args.project:
142         projects = [args.project]
143     else:
144         projects = PROJECTS
146     report_generator = ReportGenerator(gl, group, projects, args.label,
147                                        args.year, args.month)
149     print("Closed issues")
150     print("=============")
151     print()
152     for closed_issue in report_generator.closed_issues():
153         print(f'- {closed_issue["title"]}')
154         print(f'  {closed_issue["web_url"]}')
155         print()