Completed
Push — master ( 20f388...bc1964 )
by Gonzalo
01:03
created

GitHubRepo   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 86
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 86
rs 10
wmc 21

7 Methods

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