-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathcli.py
More file actions
261 lines (237 loc) · 7.73 KB
/
cli.py
File metadata and controls
261 lines (237 loc) · 7.73 KB
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
import argparse
import json
import os
import sys
from subprocess import PIPE
from subprocess import run
from .git import _git_installed_check
from .git import _git_toplevel_path
from .github_activity import _parse_target
from .github_activity import generate_activity_md
from .github_activity import generate_all_activity_md
DESCRIPTION = "Generate a markdown changelog of GitHub activity within a date window."
# These defaults are managed by load_config_and_defaults so that they can be
# overridden by a config file
ARG_DEFAULTS = {
"heading-level": 1,
"include-issues": False,
"include-opened": False,
"strip-brackets": False,
"all": False,
"ignore-contributor": [],
}
parser = argparse.ArgumentParser(description=DESCRIPTION)
parser.add_argument(
"-t",
"--target",
nargs="?",
default=None,
help="""The GitHub organization/repo for which you want to grab recent issues/PRs.
Can either be *just* an organization (e.g., `jupyter`), or a combination
organization and repo (e.g., `jupyter/notebook`). If the former, all
repositories for that org will be used. If the latter, only the specified
repository will be used. Can also be a GitHub URL to an organization or repo. If
None, the org/repo will attempt to be inferred from `git remote -v`.""",
)
parser.add_argument(
"-s",
"--since",
nargs="?",
default=None,
help="""Return issues/PRs with activity since this date or git reference. Can be
any string that is parsed with dateutil.parser.parse.""",
)
parser.add_argument(
"-u",
"--until",
nargs="?",
default=None,
help="""Return issues/PRs with activity until this date or git reference. Can be
any string that is parsed with dateutil.parser.parse. If none, today's
date will be used.""",
)
parser.add_argument(
"-o", "--output", default=None, help="""Write the markdown to a file if desired."""
)
parser.add_argument(
"--kind",
default=None,
help="""Return only issues or PRs. If None, both will be returned.""",
)
parser.add_argument(
"--auth",
default=None,
help=(
"An authentication token for GitHub. If None, then the environment "
"variable `GITHUB_ACCESS_TOKEN` will be tried. If it does not exist "
"then attempt to infer the token from `gh auth status -t`."
),
)
parser.add_argument(
"--tags",
default=None,
help=(
"A list of the tags to use in generating subsets of PRs for the "
"markdown report. Must be one of:"
""
" ['api_change', 'new', 'enhancement', 'bug', 'maintenance', 'documentation', 'ci', 'deprecate']"
""
"If None, all of the above tags will be used."
),
)
parser.add_argument(
"--include-issues",
default=None,
action="store_true",
help="Include Issues in the markdown output",
)
parser.add_argument(
"--include-opened",
default=None,
action="store_true",
help="Include a list of opened items in the markdown output",
)
parser.add_argument(
"--strip-brackets",
default=None,
action="store_true",
help=(
"If True, strip any text between brackets at the beginning of the issue/PR title. "
"E.g., [MRG], [DOC], etc."
),
)
parser.add_argument(
"--heading-level",
default=None,
type=int,
help=(
"""Base heading level to add when generating markdown.
Useful when including changelog output in an existing document.
By default, changelog is emitted with one h1 and an h2 heading for each section.
--heading-level=2 starts at h2, etc.
"""
),
)
parser.add_argument(
"--branch",
"-b",
default=None,
help=(
"""Filter pull requests by their target branch. Only PRs merged into this branch will be included. """
),
)
parser.add_argument(
"--all",
default=None,
action="store_true",
help=("""Whether to include all the GitHub tags"""),
)
parser.add_argument(
"--ignore-contributor",
action="append",
help="Do not include this GitHub username as a contributor in the changelog",
)
# Hidden argument so that target can be optionally passed as a positional argument
parser.add_argument(
"_target",
nargs="?",
default=None,
help=argparse.SUPPRESS,
)
def load_config_and_defaults(args):
"""
Load .githubactivity.json from the Git top-level directory,
override unset args with values from .githubactivity.json,
and set defaults for remaining args.
"""
config = {}
git_toplevel = _git_toplevel_path()
if git_toplevel:
try:
with open(os.path.join(git_toplevel, ".githubactivity.json")) as f:
config = json.load(f)
except FileNotFoundError:
pass
# Treat args as a dict
# https://docs.python.org/3/library/argparse.html#the-namespace-object
for argname in vars(args):
configname = argname.replace("_", "-")
if getattr(args, argname) is None:
setattr(args, argname, config.get(configname, ARG_DEFAULTS.get(configname)))
def main():
if not _git_installed_check():
print("git is required to run github-activity", file=sys.stderr)
sys.exit(1)
args = parser.parse_args()
if args.target and args._target:
raise ValueError(
"target cannot be passed as both a positional and keyword argument"
)
if not args.target:
args.target = args._target
load_config_and_defaults(args)
tags = args.tags.split(",") if args.tags is not None else args.tags
# Automatically detect the target from remotes if we haven't had one passed.
if not args.target:
err = "Could not automatically detect remote, and none was given."
try:
out = run("git remote -v".split(), stdout=PIPE)
remotes = out.stdout.decode().split("\n")
remotes = [ii for ii in remotes if ii]
remotes = {
ii.split("\t")[0]: ii.split("\t")[1].split()[0] for ii in remotes
}
if "upstream" in remotes:
ref = remotes["upstream"]
elif "origin" in remotes:
ref = remotes["origin"]
else:
ref = None
if not ref:
raise ValueError(err)
org, repo = _parse_target(ref)
if repo:
args.target = f"{org}/{repo}"
else:
args.target = f"{org}"
except Exception:
raise ValueError(err)
common_kwargs = dict(
kind=args.kind,
auth=args.auth,
tags=tags,
include_issues=bool(args.include_issues),
include_opened=bool(args.include_opened),
strip_brackets=bool(args.strip_brackets),
branch=args.branch,
ignored_contributors=args.ignore_contributor,
)
# Wrap in a try/except so we don't have an ugly stack trace if there's an error
try:
if args.all:
md = generate_all_activity_md(args.target, **common_kwargs)
else:
md = generate_activity_md(
args.target,
since=args.since,
until=args.until,
heading_level=args.heading_level,
**common_kwargs,
)
if not md:
return
if args.output:
output = os.path.abspath(args.output)
output_dir = os.path.dirname(output)
if not os.path.exists(output_dir):
os.makedirs(output_dir)
with open(args.output, "w") as ff:
ff.write(md)
print(f"Finished writing markdown to: {args.output}", file=sys.stderr)
else:
print(md)
except ValueError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()