-
Notifications
You must be signed in to change notification settings - Fork 5
/
repo_archiver.py
executable file
·361 lines (323 loc) · 11.9 KB
/
repo_archiver.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
#!/usr/bin/env python
"""
Script for archiving repo
Takes an Owner/repo and does the following
Creates a label "ARCHIVED" in red (or specified)
Applies that label to all open issues and PR's
prepends "DEPRECATED - " to the description
Archives the repo
"""
import sys
import getch
from github3 import exceptions as gh_exceptions
from github3 import login
from github_scripts import utils
# TODO: CUSTOM LABEL TEXT
MAX_CUSTOM_LENGTH = 50 - len("ARCHIVED - " + " - ")
def parse_args():
"""
Go through the command line.
If no token is specified prompt for it.
:return: Returns the parsed CLI datastructures.
"""
parser = utils.GH_ArgParser(
description="Archive the specified repo, labelling and then closing out issues and PRs, "
"per GitHub best practices. Closed issues/PRs, and description/topic changes "
"can be completely reversed using the repo_unarchiver script. "
"DEFAULTS to dry-run and will not modify things until --do-it flag is applied. "
"Also, will report on any existing hooks or keys in the repos so that cleanup in related systems can occur"
)
parser.add_argument("repos", help="owner/repo to archive", nargs="*", action="store")
parser.add_argument(
"--inactive",
help="Change the 'abandoned' and 'deprecated' wording to 'inactive'",
action="store_true",
)
parser.add_argument(
"--custom",
help=f"Custom text to add to issue/PR label, and description, less than {MAX_CUSTOM_LENGTH} char long",
type=str,
action="store",
)
parser.add_argument(
"--file", help='File with "owner/repo" one per line to archive', action="store"
)
parser.add_argument(
"--disable-report",
help="Disable the hook/keys report at the end of the process.",
action="store_false",
dest="show_report",
)
parser.add_argument(
"--ignore-issue-label",
help="Ignore the existence of the ARCHIVED issue label",
action="store_true",
dest="ignore_issue_label",
)
parser.add_argument(
"--pause",
help="Pause upon detecting anomalies that might need fixing, but aren't blockers",
action="store_true",
)
parser.add_argument(
"-q",
help="DO NOT print, or request confirmations",
dest="quiet",
action="store_true",
default=False,
)
parser.add_argument(
"--do-it",
help="Actually perform the archiving steps",
action="store_true",
dest="do_it",
)
args = parser.parse_args()
if args.repos is None and args.file is None:
raise Exception("Must have either a list of repos, OR a file to read repos from")
if args.custom is not None and len(args.custom) > MAX_CUSTOM_LENGTH:
raise Exception(f"Custom string must be less than {MAX_CUSTOM_LENGTH} characters")
return args
def handle_issues(repo, custom, ignore_label=False, do_it=False, quiet=False):
"""
Handle labelling the issues and closing them out reversibly
:param repo: the initialized repo object
:param custom: additional custom text for label
:param ignore_label: if we run into a label conflict, do we barrel through?
:param do_it: If true, we actually touch things.
:param quiet: should we talk out loud?
:return: True is all is well, False if there was an exception that we handled
"""
result = True
if not quiet:
print("\tcreating archive label")
labellist = repo.labels()
if custom is None:
labelname = "ARCHIVED"
else:
labelname = "ARCHIVED - " + custom
print(f"\tLabelname is {labelname}")
need_flag = True
for label in labellist:
if label.name.find(labelname) != -1:
need_flag = False
if not ignore_label:
print(
"Uh oh. ARCHIVED label already exists? Closing out so I don"
"t "
"step on other processes"
)
sys.exit()
if need_flag and do_it:
repo.create_label(
name=labelname, color="#c41a1a", description="CLOSED at time of archiving"
)
if not quiet:
print(f"\tStarting work on {repo.open_issues_count} issues")
issues = repo.issues(state="open")
# Need to do two passes - if we do one pass, the closure erases the label
for issue in issues:
# update label
if do_it:
issue.add_labels(labelname)
for issue in issues:
try:
if do_it:
issue.close()
if not quiet:
print(f"\tLabeled and closed issue: {issue.title}")
except gh_exceptions.UnprocessableEntity:
result = False
print(
f"Got 422 Unproccessable on issue {issue.title},"
" continuing. May need to manually finish closing."
)
return result
def handle_topics(gh_repo, topic_inactive, do_it=False, quiet=False):
"""
Given a repo, update the topics to indicate its inactivity - either the default ABANDONED language or INACTIVE if desired.
:param gh_repo: the initialized repo object
:param topic_inactive: boolean, should we use the milder language
:param do_it: If true, we actually touch things.
:param quiet: do we output anything?
No return value
"""
topics = gh_repo.topics().names
if do_it:
if topic_inactive:
topics.append("inactive")
else:
topics.append("abandoned")
topics.append("unmaintained")
gh_repo.replace_topics(topics)
if not quiet:
print("\tUpdated topics")
def handle_hooks(gh_repo, ignore_hooks=False, disable_hooks=False):
"""
Given an initialized repo, look for hooks.
If hooks are found, disable them if asked to
Return bool if there are any hooks still enabled, unless ignore is set
:param gh_repo: initialized repo object
:param ignore_hooks: just pretend everything is fine
:param disable_hooks: disable existing hooks
return: True if there are any hooks
"""
# are there hooks
hooklist = list(gh_repo.hooks())
if len(hooklist) > 0:
hooks_exist = True
else:
hooks_exist = False
hooksdisabled = True
if disable_hooks:
for hook in hooklist:
if not hook.edit(active=False):
hooksdisabled = False # Something went wrong trying to disable.
if ignore_hooks:
return False
else:
return hooks_exist and not hooksdisabled
def handle_keys(gh_repo, ignore_keys=False, delete_keys=False):
"""
Given an initialized repo, look for keys.
If keys are found, delete them if asked to
Return bool if there are any keys existing, unless ignore is set
:param gh_repo: initialized repo object
:param ignore_keys: just pretend everything is fine
:param delete_keys: delete existing keys
return: True if there are any keys
"""
keylist = list(gh_repo.keys())
if len(keylist) > 0:
keys_exist = True
else:
keys_exist = False
if delete_keys:
for key in keylist:
key.delete()
if ignore_keys:
return False
else:
return keys_exist and not delete_keys
def report_on_hooks(repo):
"""
Return a list of strings, "org,repo,hookURL,boolEnabled" for each hook found
:param: the initialized repo object
:result: a list of strings
"""
result = []
for hook in repo.hooks():
result.append(f"HOOK,{repo.owner.login},{repo.name},{hook.config['url']},{hook.active}")
return result
def report_on_keys(repo):
"""
Return a list of strings, "org,repo,keyTitle,keycreated,keylastused" for each key found
:param: the initialized repo object
:result: a list of strings
"""
result = []
for key in repo.keys():
result.append(
f"KEY,{repo.owner.login},{repo.name},{key.title},{key.created_at},{key.last_used}"
)
return result
def main():
"""
Main logic for the archiver
"""
args = parse_args()
gh_sess = login(token=args.token)
key_report_list = ["type,org,repo,key"]
hook_report_list = ["type,org,repo,hookURL,status"]
repolist = []
if args.repos != []:
repolist = args.repos
elif args.file:
try:
# Rip open the file, make a list
txtfile = open(args.file, "r")
repolist = txtfile.readlines()
txtfile.close()
except Exception:
print("Problem loading file!")
return
else:
print("Please specify an org/repo or a file.")
return
for orgrepo in repolist:
try:
org = orgrepo.split("/")[0].strip()
repo = orgrepo.split("/")[1].strip()
except IndexError:
print(f"{orgrepo} needs to be in the form ORG/REPO")
sys.exit()
try:
gh_repo = gh_sess.repository(owner=org, repository=repo)
except gh_exceptions.NotFoundError:
print(f"Trying to open {org}/{repo}, failed with 404")
sys.exit()
if gh_repo.archived:
if not args.quiet:
print(f"repo {org}/{repo} is already archived, skipping")
else:
if not args.quiet:
print(f"working with repo: {org}/{repo}")
# If there are gh_pages - let people know about it.
if gh_repo.has_pages:
if args.pause:
print(
"\tNOTE: Repo has gh_pages - please deal with them in the UI and press any key to continue"
)
char = getch.getch()
else:
print("\tNOTE: Repo has gh_pages")
# Look for keys and hooks, and report on them at the end
if args.show_report:
key_report_list.extend(report_on_keys(gh_repo))
hook_report_list.extend(report_on_hooks(gh_repo))
# Deal with issues
handled = handle_issues(
gh_repo, args.custom, args.ignore_issue_label, args.do_it, args.quiet
)
# Handle the overall repo marking:
handle_topics(gh_repo, args.inactive, args.do_it, args.quiet)
description = gh_repo.description
if args.inactive:
preamble = "INACTIVE"
else:
preamble = "DEPRECATED"
if args.custom is not None:
preamble += " - " + args.custom
if description is not None:
description = preamble + " - " + description
else:
description = preamble
if handled:
if args.do_it:
gh_repo.edit(name=gh_repo.name, description=description, archived=True)
if not args.quiet:
print(f"\tUpdated description and archived the repo {org}/{repo}")
elif True:
if args.do_it:
gh_repo.edit(name=gh_repo.name, description=description)
print(
f"\tUpdated description, but there was a problem with issues in repo "
f"https://github.com/{org}/{repo}, pausing so you can fix, and then "
f"I'll archive for you. (Press enter to archive, N and enter to skip)"
)
char = input()
if char not in ("n", "N"):
gh_repo.edit(name=gh_repo.name, archived=True)
if not args.quiet:
print(f"\tArchived repo {org}/{repo}")
else:
if not args.quiet:
print(f"\tDid NOT archive {org}/{repo}")
if args.show_report:
print()
print("\n".join(hook_report_list))
print("---------------")
print("\n".join(key_report_list))
print("---------------")
if __name__ == "__main__":
main()