Completed
Push — master ( 5509a6...012178 )
by Gonzalo
02:05 queued 56s
created

GitHubRepo.issues()   F

Complexity

Conditions 9

Size

Total Lines 49

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 9
c 2
b 0
f 0
dl 0
loc 49
rs 3.75
1
# -*- coding: utf-8 -*-
2
# -----------------------------------------------------------------------------
3
# Copyright (c) The Spyder Development Team
4
#
5
# Licensed under the terms of the MIT License
6
# (See LICENSE.txt for details)
7
# -----------------------------------------------------------------------------
8
"""Build a list of issues and pull requests per Github milestone."""
9
10
from __future__ import print_function
11
12
# Standard library imports
13
import argparse
14
import datetime
15
import getpass
16
import sys
17
import time
18
19
# Local imports
20
from loghub.external.github import GitHub
21
22
PY2 = sys.version[0] == '2'
23
24
# TEMPLATES
25
ISSUE_LONG = "* [Issue {number}](https://github.com/{repo}/issues/{number})"
26
ISSUE_SHORT = "* Issue #{number}"
27
PR_LONG = "* [PR {number}](https://github.com/{repo}/pull/{number})"
28
PR_SHORT = "* PR #{number}"
29
30
31
def main():
32
    """Main script."""
33
    # Cli options
34
    parser = argparse.ArgumentParser(
35
        description='Script to print the list of issues and pull requests '
36
        'closed in a given milestone')
37
    parser.add_argument(
38
        'repository',
39
        help="Repository name to generate the Changelog for, in the form "
40
        "user/repo or org/repo (e.g. spyder-ide/spyder)")
41
    parser.add_argument(
42
        '-m',
43
        '--milestone',
44
        action="store",
45
        dest="milestone",
46
        default='',
47
        help="Github milestone to get issues and pull requests for")
48
    parser.add_argument(
49
        '-st',
50
        '--since-tag',
51
        action="store",
52
        dest="since_tag",
53
        default='',
54
        help="Github issues and pull requests since tag")
55
    parser.add_argument(
56
        '-ut',
57
        '--until-tag',
58
        action="store",
59
        dest="until_tag",
60
        default='',
61
        help="Github issues and pull requests until tag")
62
    parser.add_argument(
63
        '-f',
64
        '--format',
65
        action="store",
66
        dest="output_format",
67
        default='changelog',
68
        help="Format for print, either 'changelog' (for "
69
        "Changelog.md file) or 'release' (for the Github "
70
        "Releases page). Default is 'changelog'. The "
71
        "'release' option doesn't generate Markdown "
72
        "hyperlinks.")
73
    parser.add_argument(
74
        '-u',
75
        '--user',
76
        action="store",
77
        dest="user",
78
        default='',
79
        help="Github user name")
80
    parser.add_argument(
81
        '-p',
82
        '--password',
83
        action="store",
84
        dest="password",
85
        default='',
86
        help="Github user password")
87
    options = parser.parse_args()
88
89
    # Check if repo given
90
    if not options.repository:
91
        print('Please define a repository name to this script. See its help')
92
        sys.exit(1)
93
94
    # Check if milestone or tag given
95
    if not options.milestone and not options.since_tag:
96
        print('Please pass a milestone or a tag to this script. See its help')
97
        sys.exit(1)
98
99
    create_changelog(
100
        repo=options.repository,
101
        username=options.user,
102
        password=options.password,
103
        milestone=options.milestone,
104
        since_tag=options.since_tag,
105
        until_tag=options.until_tag,
106
        output_format=options.output_format, )
107
108
109
def create_changelog(repo, username, password, milestone, since_tag, until_tag,
110
                     output_format):
111
    """Create changelog data."""
112
    if username and not password:
113
        password = getpass.getpass()
114
115
    # Instantiate Github API
116
    gh = GitHubRepo(username=username, password=password, repo=repo)
117
118
    version = until_tag or None
119
    milestone_number = None
120
    closed_at = None
121
    since = None
122
    until = None
123
124
    # Set milestone or from tag
125
    if milestone and not since_tag:
126
        milestone_data = gh.milestone(milestone)
127
        milestone_number = milestone_data['number']
128
        closed_at = milestone_data['closed_at']
129
        version = milestone.replace('v', '')
130
    elif not milestone and since_tag:
131
        since = gh.tag(since_tag)['tagger']['date']
132
        if until_tag:
133
            until = gh.tag(until_tag)['tagger']['date']
134
            closed_at = until
135
136
    # This returns issues and pull requests
137
    issues = gh.issues(
138
        milestone=milestone_number, state='closed', since=since, until=until)
139
140
    format_changelog(
141
        repo,
142
        issues,
143
        version,
144
        closed_at=closed_at,
145
        output_format=output_format)
146
147
148
def format_changelog(repo,
149
                     issues,
150
                     version,
151
                     closed_at=None,
152
                     output_format='changelog',
153
                     output_file='CHANGELOG.temp'):
154
    """Create changelog data."""
155
    lines = []
156
157
    # Header
158
    if version and version[0] == 'v':
159
        version = version.replace('v', '')
160
    else:
161
        '<RELEASE_VERSION>'
162
163
    if closed_at:
164
        close_date = closed_at.split('T')[0]
165
    else:
166
        close_date = time.strftime("%Y/%m/%d")
167
168
    quotes = '"' if version and ' ' in version else ''
169
    header = '## Version {q}{version}{q} ({date})\n'.format(
170
        version=version, date=close_date, q=quotes)
171
172
    lines.append(header)
173
174
    # --- Issues
175
    number_of_issues = 0
176
    issue_lines = ['### Issues Closed\n']
177
    for i in issues:
178
        pr = i.get('pull_request', '')
179
        if not pr:
180
            number_of_issues += 1
181
            number = i['number']
182
            if output_format == 'changelog':
183
                issue_link = ISSUE_LONG.format(number=number, repo=repo)
184
            else:
185
                issue_link = ISSUE_SHORT.format(number=number)
186
            issue_lines.append(issue_link + ' - ' + i['title'])
187
188
    tense = 'was' if number_of_issues == 1 else 'were'
189
    plural = '' if number_of_issues == 1 else 's'
190
    issue_lines.append('\nIn this release {number} issue{plural} {tense} '
191
                       'closed\n'.format(
192
                           number=number_of_issues, tense=tense,
193
                           plural=plural))
194
    if number_of_issues > 0:
195
        lines = lines + issue_lines
196
197
    # --- Pull requests
198
    number_of_prs = 0
199
    pr_lines = ['### Pull Requests merged\n']
200
    for i in issues:
201
        pr = i.get('pull_request', '')
202
        if pr:
203
            number_of_prs += 1
204
            number = i['number']
205
            if output_format == 'changelog':
206
                pr_link = PR_LONG.format(number=number, repo=repo)
207
            else:
208
                pr_link = PR_SHORT.format(number=number)
209
            pr_lines.append(pr_link + ' - ' + i['title'])
210
    tense = 'was' if number_of_prs == 1 else 'were'
211
    plural = '' if number_of_prs == 1 else 's'
212
    pr_lines.append('\nIn this release {number} pull request{plural} {tense} '
213
                    'merged\n'.format(
214
                        number=number_of_prs, tense=tense, plural=plural))
215
    if number_of_prs > 0:
216
        lines = lines + pr_lines
217
218
    # Print everything
219
    for line in lines:
220
        # Make the text file and console output identical
221
        if line.endswith('\n'):
222
            line = line[:-1]
223
        print(line)
224
    print()
225
226
    # Write to file
227
    text = ''.join(lines)
228
229
    if PY2:
230
        text = unicode(text).encode('utf-8')  # NOQA
231
232
    with open(output_file, 'w') as f:
233
        f.write(text)
234
235
236
class GitHubRepo(object):
237
    """Github repository wrapper."""
238
239
    def __init__(self, username=None, password=None, repo=None):
240
        """Github repository wrapper."""
241
        self.gh = GitHub(username=username, password=password)
242
        repo_organization, repo_name = repo.split('/')
243
        self.repo = self.gh.repos(repo_organization)(repo_name)
244
245
    def tags(self):
246
        """Return all tags."""
247
        return self.repo('git')('refs')('tags').get()
248
249
    def tag(self, tag_name):
250
        """Get tag information."""
251
        refs = self.repo('git')('refs')('tags').get()
252
        sha = -1
253
        for ref in refs:
254
            ref_name = 'refs/tags/{tag}'.format(tag=tag_name)
255
            if 'object' in ref and ref['ref'] == ref_name:
256
                sha = ref['object']['sha']
257
                break
258
259
        if sha == -1:
260
            print("You didn't pass a valid tag name!")
261
            sys.exit(1)
262
263
        return self.repo('git')('tags')(sha).get()
264
265
    def milestones(self):
266
        """Return all milestones."""
267
        return self.repo.milestones.get(state='all')
268
269
    def milestone(self, milestone_title):
270
        """Return milestone with given title."""
271
        milestones = self.milestones()
272
        milestone_number = -1
273
        for milestone in milestones:
274
            if milestone['title'] == milestone_title:
275
                milestone_number = milestone['number']
276
                break
277
278
        if milestone_number == -1:
279
            print("You didn't pass a valid milestone name!")
280
            sys.exit(1)
281
282
        return milestone
283
284
    def issues(self,
285
               milestone=None,
286
               state=None,
287
               assignee=None,
288
               creator=None,
289
               mentioned=None,
290
               labels=None,
291
               sort=None,
292
               direction=None,
293
               since=None,
294
               until=None):
295
        """Return Issues and Pull Requests."""
296
        page = 1
297
        issues = []
298
        while True:
299
            result = self.repo.issues.get(page=page,
300
                                          per_page=100,
301
                                          milestone=milestone,
302
                                          state=state,
303
                                          assignee=assignee,
304
                                          creator=creator,
305
                                          mentioned=mentioned,
306
                                          labels=labels,
307
                                          sort=sort,
308
                                          firection=direction,
309
                                          since=since)
310
            if len(result) > 0:
311
                issues += result
312
                page = page + 1
313
            else:
314
                break
315
316
        # If since was provided, filter the issue
317
        if since:
318
            since_date = self.str_to_date(since)
319
            for issue in issues[:]:
320
                close_date = self.str_to_date(issue['closed_at'])
321
                if close_date < since_date:
322
                    issues.remove(issue)
323
324
        # If until was provided, filter the issue
325
        if until:
326
            until_date = self.str_to_date(until)
327
            for issue in issues[:]:
328
                close_date = self.str_to_date(issue['closed_at'])
329
                if close_date > until_date:
330
                    issues.remove(issue)
331
332
        return issues
333
334
    @staticmethod
335
    def str_to_date(string):
336
        """Convert ISO date string to datetime object."""
337
        parts = string.split('T')
338
        date_parts = parts[0]
339
        time_parts = parts[1][:-1]
340
        year, month, day = [int(i) for i in date_parts.split('-')]
341
        hour, minutes, seconds = [int(i) for i in time_parts.split(':')]
342
        return datetime.datetime(year, month, day, hour, minutes, seconds)
343
344
345
if __name__ == '__main__':
346
    main()
347