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 [
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):
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
59 assert isinstance(project, gitlab.v4.objects.Project)
63 class ReportGenerator(object):
64 def __init__(self, gl, group, projects: list, label: str, year: int,
68 self.projects = projects
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:
75 project = self.gl.project_from_name(project_name)
76 closed_issues_events = project.events.list(as_list=False,
82 gl_closed_issues_with_duplicates = [{
83 "project_id": event.project_id,
84 "iid": event.target_iid
85 } for event in closed_issues_events]
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:
96 closed_issues.append({
98 "web_url": issue.web_url,
103 def closed_issues(self) -> list:
105 for project in self.projects:
106 closed_issues = closed_issues + self.closed_issues_in_project(
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__':
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()
130 logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT)
132 logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)
134 gl = GitLabWrapper.from_config(PYTHON_GITLAB_NAME,
135 config_files=[PYTHON_GITLAB_CONFIG_FILE])
138 group = gl.groups.list(search=GROUP_NAME)[0]
139 assert isinstance(group, gitlab.v4.objects.Group)
142 projects = [args.project]
146 report_generator = ReportGenerator(gl, group, projects, args.label,
147 args.year, args.month)
149 print("Closed issues")
150 print("=============")
152 for closed_issue in report_generator.closed_issues():
153 print(f'- {closed_issue["title"]}')
154 print(f' {closed_issue["web_url"]}')