3 # Documentation: https://tails.boum.org/contribute/working_together/GitLab/#api
9 from datetime import datetime
12 import gitlab # type: ignore
14 sys.exit("You need to install python3-gitlab to use this program.")
17 from dateutil.relativedelta import relativedelta
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',
27 PYTHON_GITLAB_NAME = os.getenv('GITLAB_NAME', default='Tails')
31 # By default, only changes in these projects are considered
33 GROUP_NAME + '/' + project for project in [
42 LOG_FORMAT = "%(asctime)-15s %(levelname)s %(message)s"
43 log = logging.getLogger()
46 class GitLabWrapper(gitlab.Gitlab):
47 @functools.lru_cache(maxsize=None)
48 def project(self, project_id):
49 return self.projects.get(project_id)
51 @functools.lru_cache(maxsize=None)
52 def project_from_name(self, project_name):
54 p for p in self.projects.list(all=True)
55 # Disambiguate between projects whose names share a common prefix
56 if p.path_with_namespace == project_name
58 assert isinstance(project, gitlab.v4.objects.Project)
62 class ReportGenerator(object):
63 def __init__(self, gl, group, projects: list, label: str, year: int,
67 self.projects = projects
69 self.after = end_of_previous_month(year, month)
70 self.before = beginning_of_next_month(year, month)
72 def closed_issues_in_project(self, project_name) -> list:
74 project = self.gl.project_from_name(project_name)
75 closed_issues_events = project.events.list(as_list=False,
81 gl_closed_issues_with_duplicates = [{
82 "project_id": event.project_id,
83 "iid": event.target_iid
84 } for event in closed_issues_events]
86 for issue in gl_closed_issues_with_duplicates:
87 if issue not in gl_closed_issues:
88 gl_closed_issues.append(issue)
90 for issue in gl_closed_issues:
91 project = self.gl.project(issue["project_id"])
92 issue = project.issues.get(issue["iid"])
93 if self.label is not None and self.label not in issue.labels:
95 closed_issues.append({
97 "web_url": issue.web_url,
102 def closed_issues(self) -> list:
104 for project in self.projects:
105 closed_issues = closed_issues + self.closed_issues_in_project(
110 def beginning_of_next_month(year, month):
111 return (datetime(year, month, 1) + relativedelta(months=1)).replace(day=1)
114 def end_of_previous_month(year, month):
115 return datetime(year, month, 1) + relativedelta(seconds=-1)
118 if __name__ == '__main__':
120 parser = argparse.ArgumentParser()
121 parser.add_argument('--year', type=int, required=True)
122 parser.add_argument('--month', type=int, required=True)
123 parser.add_argument('--label', default=None)
124 parser.add_argument('--project')
125 parser.add_argument("--debug", action="store_true", help="debug output")
126 args = parser.parse_args()
129 logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT)
131 logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)
133 gl = GitLabWrapper.from_config(PYTHON_GITLAB_NAME,
134 config_files=[PYTHON_GITLAB_CONFIG_FILE])
137 group = gl.groups.list(search=GROUP_NAME)[0]
138 assert isinstance(group, gitlab.v4.objects.Group)
141 projects = [args.project]
145 report_generator = ReportGenerator(gl, group, projects, args.label,
146 args.year, args.month)
148 print("Closed issues")
149 print("=============")
151 for closed_issue in report_generator.closed_issues():
152 print(f'- {closed_issue["title"]}')
153 print(f' {closed_issue["web_url"]}')