Completed
Pull Request — master (#13)
by Gonzalo
55s
created

GitHubRepo.milestones()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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