|
1
|
+#!/usr/bin/env python3
|
|
2
|
+from datetime import datetime
|
|
3
|
+import enum
|
|
4
|
+from pathlib import Path
|
|
5
|
+import sys
|
|
6
|
+
|
|
7
|
+import requests
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+GITLAB = "https://gitlab.torproject.org"
|
|
11
|
+API_URL = f"{GITLAB}/api/v4"
|
|
12
|
+PROJECT_ID = 473
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+class Platform(enum.IntFlag):
|
|
16
|
+ WINDOWS = 8
|
|
17
|
+ MACOS = 4
|
|
18
|
+ LINUX = 2
|
|
19
|
+ ANDROID = 1
|
|
20
|
+ DESKTOP = 8 | 4 | 2
|
|
21
|
+ ALL_PLATFORMS = 8 | 4 | 2 | 1
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+class Issue:
|
|
25
|
+ def __init__(self, j):
|
|
26
|
+ self.title = j["title"]
|
|
27
|
+ self.project, self.number = (
|
|
28
|
+ j["references"]["full"].rsplit("/", 2)[-1].split("#")
|
|
29
|
+ )
|
|
30
|
+ self.platform = 0
|
|
31
|
+ self.num_platforms = 0
|
|
32
|
+ if "Desktop" in j["labels"]:
|
|
33
|
+ self.platform = Platform.DESKTOP
|
|
34
|
+ self.num_platforms += 3
|
|
35
|
+ else:
|
|
36
|
+ if "Windows" in j["labels"]:
|
|
37
|
+ self.platform |= Platform.WINDOWS
|
|
38
|
+ self.num_platforms += 1
|
|
39
|
+ if "MacOS" in j["labels"]:
|
|
40
|
+ self.platform |= Platform.MACOS
|
|
41
|
+ self.num_platforms += 1
|
|
42
|
+ if "Linux" in j["labels"]:
|
|
43
|
+ self.platform |= Platform.LINUX
|
|
44
|
+ self.num_platforms += 1
|
|
45
|
+ if "Android" in j["labels"]:
|
|
46
|
+ self.platform |= Platform.ANDROID
|
|
47
|
+ self.num_platforms += 1
|
|
48
|
+ if not self.platform:
|
|
49
|
+ self.platform = Platform.ALL_PLATFORMS
|
|
50
|
+ self.num_platforms = 4
|
|
51
|
+
|
|
52
|
+ def get_platforms(self):
|
|
53
|
+ if self.platform == Platform.ALL_PLATFORMS:
|
|
54
|
+ return "All Platforms"
|
|
55
|
+ platforms = []
|
|
56
|
+ if self.platform & Platform.WINDOWS:
|
|
57
|
+ platforms.append("Windows")
|
|
58
|
+ if self.platform & Platform.MACOS:
|
|
59
|
+ platforms.append("macOS")
|
|
60
|
+ if self.platform & Platform.LINUX:
|
|
61
|
+ platforms.append("Linux")
|
|
62
|
+ if self.platform & Platform.ANDROID:
|
|
63
|
+ platforms.append("Android")
|
|
64
|
+ return " + ".join(platforms)
|
|
65
|
+
|
|
66
|
+ def __str__(self):
|
|
67
|
+ return f"Bug {self.number}: {self.title} [{self.project}]"
|
|
68
|
+
|
|
69
|
+ def __lt__(self, other):
|
|
70
|
+ return self.number < other.number
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+if len(sys.argv) < 2:
|
|
74
|
+ print(f"Usage: {sys.argv[0]} version-to-release or #issue-id")
|
|
75
|
+ sys.exit(1)
|
|
76
|
+
|
|
77
|
+token_file = Path(__file__).parent / ".changelogs_token"
|
|
78
|
+if not token_file.exists():
|
|
79
|
+ print(
|
|
80
|
+ f"Please add your personal GitLab token (with 'read_api' scope) to {token_file}"
|
|
81
|
+ )
|
|
82
|
+ print(
|
|
83
|
+ f"Please go to {GITLAB}/-/profile/personal_access_tokens and generate it."
|
|
84
|
+ )
|
|
85
|
+ token = input("Please enter the new token: ").strip()
|
|
86
|
+ if not token:
|
|
87
|
+ print("Invalid token!")
|
|
88
|
+ sys.exit(2)
|
|
89
|
+ with token_file.open("w") as f:
|
|
90
|
+ f.write(token)
|
|
91
|
+with token_file.open() as f:
|
|
92
|
+ token = f.read().strip()
|
|
93
|
+headers = {"PRIVATE-TOKEN": token}
|
|
94
|
+
|
|
95
|
+if sys.argv[1][0] != "#":
|
|
96
|
+ version = sys.argv[1]
|
|
97
|
+ r = requests.get(
|
|
98
|
+ f"{API_URL}/projects/{PROJECT_ID}/issues?labels=Release Prep",
|
|
99
|
+ headers=headers,
|
|
100
|
+ )
|
|
101
|
+ issue = None
|
|
102
|
+ for i in r.json():
|
|
103
|
+ if i["title"].find(sys.argv[1]) != -1:
|
|
104
|
+ if issue is None:
|
|
105
|
+ issue = i
|
|
106
|
+ else:
|
|
107
|
+ print("More than one matching issue found!")
|
|
108
|
+ print("Please use the issue id.")
|
|
109
|
+ sys.exit(3)
|
|
110
|
+ if not issue:
|
|
111
|
+ print(
|
|
112
|
+ "Release preparation issue not found. Please make sure it has ~Release Prep."
|
|
113
|
+ )
|
|
114
|
+ sys.exit(4)
|
|
115
|
+ iid = issue["iid"]
|
|
116
|
+else:
|
|
117
|
+ version = "????"
|
|
118
|
+ iid = sys.argv[1][1:]
|
|
119
|
+
|
|
120
|
+r = requests.get(
|
|
121
|
+ f"{API_URL}/projects/{PROJECT_ID}/issues/{iid}/links", headers=headers
|
|
122
|
+)
|
|
123
|
+linked = {}
|
|
124
|
+for i in r.json():
|
|
125
|
+ i = Issue(i)
|
|
126
|
+ if i.platform not in linked:
|
|
127
|
+ linked[i.platform] = []
|
|
128
|
+ linked[i.platform].append(i)
|
|
129
|
+linked = sorted(
|
|
130
|
+ linked.values(),
|
|
131
|
+ key=lambda issues: (issues[0].num_platforms << 8) | issues[0].platform,
|
|
132
|
+ reverse=True,
|
|
133
|
+)
|
|
134
|
+
|
|
135
|
+date = datetime.now().strftime("%B %d %Y")
|
|
136
|
+print(f"Tor Browser {version} - {date}")
|
|
137
|
+for issues in linked:
|
|
138
|
+ print(f" * {issues[0].get_platforms()}")
|
|
139
|
+ for i in sorted(issues):
|
|
140
|
+ print(f" * {i}") |