Completed
Push — master ( 40212d...576f4e )
by Gonzalo
59s
created

GitHubRepo.milestone()   B

Complexity

Conditions 5

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 7
Bugs 0 Features 1
Metric Value
cc 5
c 7
b 0
f 1
dl 0
loc 19
rs 8.5454
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
"""Github repo wrapper."""
9
10
from __future__ import print_function
11
12
# Standard library imports
13
import datetime
14
import sys
15
import time
16
17
# Local imports
18
from loghub.external.github import ApiError, ApiNotFoundError, GitHub
19
20
21
class GitHubRepo(object):
22
    """Github repository wrapper."""
23
24
    def __init__(self, username=None, password=None, token=None, repo=None):
25
        """Github repository wrapper."""
26
        self._username = username
27
        self._password = password
28
        self._token = token
29
30
        self.gh = GitHub(
31
            username=username,
32
            password=password,
33
            access_token=token, )
34
        repo_organization, repo_name = repo.split('/')
35
        self._repo_organization = repo_organization
36
        self._repo_name = repo_name
37
        self.repo = self.gh.repos(repo_organization)(repo_name)
38
39
        # Check username and repo name
40
        self._check_user()
41
        self._check_repo_name()
42
43
    def _check_user(self):
44
        """Check if the supplied username is valid."""
45
        try:
46
            self.gh.users(self._repo_organization).get()
47
        except ApiNotFoundError:
48
            print('LOGHUB: Organization/user `{}` seems to be '
49
                  'invalid.\n'.format(self._repo_organization))
50
            sys.exit(1)
51
        except ApiError:
52
            self._check_rate()
53
            print('LOGHUB: The credentials seems to be invalid!\n')
54
            sys.exit(1)
55
56
    def _check_repo_name(self):
57
        """Check if the supplied repository exists."""
58
        try:
59
            self.repo.get()
60
        except ApiNotFoundError:
61
            print('LOGHUB: Repository `{0}` for organization/username `{1}` '
62
                  'seems to be invalid.\n'.format(self._repo_name,
63
                                                  self._repo_organization))
64
            sys.exit(1)
65
        except ApiError:
66
            self._check_rate()
67
68
    def _check_rate(self):
69
        """Check and handle if api rate limit has been exceeded."""
70
        if self.gh.x_ratelimit_remaining == 0:
71
            reset_struct = time.gmtime(self.gh.x_ratelimit_reset)
72
            reset_format = time.strftime('%Y/%m/%d %H:%M', reset_struct)
73
            print('LOGHUB: GitHub API rate limit exceeded!')
74
            print('LOGHUB: GitHub API rate limit resets on '
75
                  '{}'.format(reset_format))
76
            if not self._username and not self._password or not self._token:
77
                print('LOGHUB: Try running loghub with user/password or '
78
                      'a valid token.\n')
79
            sys.exit(1)
80
81
    def _filter_since(self, issues, since):
82
        """Filter out all issues before `since` date."""
83
        if since:
84
            since_date = self.str_to_date(since)
85
            for issue in issues[:]:
86
                close_date = self.str_to_date(issue['closed_at'])
87
                if close_date < since_date and issue in issues:
88
                    issues.remove(issue)
89
        return issues
90
91
    def _filter_until(self, issues, until):
92
        """Filter out all issues after `until` date."""
93
        if until:
94
            until_date = self.str_to_date(until)
95
            for issue in issues[:]:
96
                close_date = self.str_to_date(issue['closed_at'])
97
                if close_date > until_date and issue in issues:
98
                    issues.remove(issue)
99
        return issues
100
101
    def _filter_by_branch(self, issues, issue, branch):
102
        """Filter prs by the branch they were merged into."""
103
        number = issue['number']
104
105
        if not self.is_merged(number) and issue in issues:
106
            issues.remove(issue)
107
108
        if branch:
109
            # Get PR info and get base branch
110
            pr_data = self.pr(number)
111
            base_ref = pr_data['base']['ref']
112
113
            if base_ref != branch and issue in issues:
114
                issues.remove(issue)
115
116
        return issues
117
118
    def _filer_closed_prs(self, issues, branch):
119
        """Filter out closed PRs."""
120
        for issue in issues[:]:
121
            pr = issue.get('pull_request', '')
122
123
            # Add label names inside additional key
124
            issue['loghub_label_names'] = [
125
                l['name'] for l in issue.get('labels')
126
            ]
127
128
            if pr:
129
                issues = self._filter_by_branch(issues, issue, branch)
130
131
        return issues
132
133
    def tags(self):
134
        """Return all tags."""
135
        self._check_rate()
136
        return self.repo('git')('refs')('tags').get()
137
138
    def tag(self, tag_name):
139
        """Get tag information."""
140
        self._check_rate()
141
        refs = self.repo('git')('refs')('tags').get()
142
        sha = -1
143
144
        tags = []
145
        for ref in refs:
146
            ref_name = 'refs/tags/{tag}'.format(tag=tag_name)
147
            if 'object' in ref and ref['ref'] == ref_name:
148
                sha = ref['object']['sha']
149
            tags.append(ref['ref'].split('/')[-1])
150
151
        if sha == -1:
152
            print("LOGHUB: You didn't pass a valid tag name!")
153
            print('LOGHUB: The available tags are: {0}\n'.format(tags))
154
            sys.exit(1)
155
156
        return self.repo('git')('tags')(sha).get()
157
158
    def milestones(self):
159
        """Return all milestones."""
160
        self._check_rate()
161
        return self.repo.milestones.get(state='all')
162
163
    def milestone(self, milestone_title):
164
        """Return milestone with given title."""
165
        self._check_rate()
166
        milestones = self.milestones()
167
        milestone_number = -1
168
169
        milestone_titles = [milestone['title'] for milestone in milestones]
170
        for milestone in milestones:
171
            if milestone['title'] == milestone_title:
172
                milestone_number = milestone['number']
173
                break
174
175
        if milestone_number == -1:
176
            print("LOGHUB: You didn't pass a valid milestone name!")
177
            print('LOGHUB: The available milestones are: {0}\n'
178
                  ''.format(milestone_titles))
179
            sys.exit(1)
180
181
        return milestone
182
183
    def pr(self, pr_number):
184
        """Get PR information."""
185
        self._check_rate()
186
        return self.repo('pulls')(str(pr_number)).get()
187
188
    def issues(self,
189
               milestone=None,
190
               state=None,
191
               assignee=None,
192
               creator=None,
193
               mentioned=None,
194
               labels=None,
195
               sort=None,
196
               direction=None,
197
               since=None,
198
               until=None,
199
               branch=None):
200
        """Return Issues and Pull Requests."""
201
        self._check_rate()
202
        page = 1
203
        issues = []
204
        while True:
205
            result = self.repo.issues.get(page=page,
206
                                          per_page=100,
207
                                          milestone=milestone,
208
                                          state=state,
209
                                          assignee=assignee,
210
                                          creator=creator,
211
                                          mentioned=mentioned,
212
                                          labels=labels,
213
                                          sort=sort,
214
                                          direction=direction,
215
                                          since=since)
216
            if len(result) > 0:
217
                issues += result
218
                page = page + 1
219
            else:
220
                break
221
222
#        print(issues)
223
        # If since was provided, filter the issue
224
        issues = self._filter_since(issues, since)
225
226
        # If until was provided, filter the issue
227
        issues = self._filter_until(issues, until)
228
229
        # If it is a pr check if it is merged or closed, removed closed ones
230
        issues = self._filer_closed_prs(issues, branch)
231
232
        return issues
233
234
    def is_merged(self, pr):
235
        """
236
        Return wether a PR was merged, or if it was closed and discarded.
237
238
        https://developer.github.com/v3/pulls/#get-if-a-pull-request-has-been-merged
239
        """
240
        self._check_rate()
241
        merged = True
242
        try:
243
            self.repo('pulls')(str(pr))('merge').get()
244
        except Exception:
245
            merged = False
246
        return merged
247
248
    @staticmethod
249
    def str_to_date(string):
250
        """Convert ISO date string to datetime object."""
251
        parts = string.split('T')
252
        date_parts = parts[0]
253
        time_parts = parts[1][:-1]
254
        year, month, day = [int(i) for i in date_parts.split('-')]
255
        hour, minutes, seconds = [int(i) for i in time_parts.split(':')]
256
        return datetime.datetime(year, month, day, hour, minutes, seconds)
257